From 7964563299b2f561cca07d0597047797cc7867a1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Mar 2026 00:24:40 -0700 Subject: [PATCH] refactor: finish plugin-owned channel runtime seams --- extensions/discord/src/channel.ts | 16 +- extensions/imessage/src/channel.ts | 1 + extensions/signal/src/channel.ts | 2 +- extensions/slack/src/channel.ts | 2 +- extensions/telegram/src/channel.ts | 2 +- package.json | 4 + scripts/lib/plugin-sdk-entrypoints.json | 1 + .../reply/commands-subagents/shared.ts | 12 +- .../reply/dispatch-from-config.test.ts | 11 ++ src/auto-reply/reply/dispatch-from-config.ts | 6 +- src/auto-reply/reply/reply-payloads.ts | 17 +- src/auto-reply/reply/telegram-context.test.ts | 11 +- src/auto-reply/reply/telegram-context.ts | 4 +- src/channels/plugins/directory-config.ts | 81 +++++++-- src/channels/plugins/exec-approval-local.ts | 22 +++ src/channels/plugins/index.ts | 91 +--------- src/channels/plugins/registry.ts | 82 +++++++++ src/channels/plugins/target-parsing.ts | 26 +++ src/channels/plugins/types.adapters.ts | 5 + src/plugin-sdk/channel-config-helpers.ts | 20 ++- src/plugin-sdk/routing.ts | 6 + src/plugins/commands.test.ts | 11 +- src/plugins/commands.ts | 16 +- src/plugins/runtime/runtime-channel.ts | 164 ++---------------- src/plugins/runtime/runtime-discord.ts | 60 +++++++ src/plugins/runtime/runtime-imessage.ts | 12 ++ src/plugins/runtime/runtime-signal.ts | 14 ++ src/plugins/runtime/runtime-slack.ts | 24 +++ src/plugins/runtime/runtime-telegram.ts | 61 +++++++ 29 files changed, 488 insertions(+), 296 deletions(-) create mode 100644 src/channels/plugins/exec-approval-local.ts create mode 100644 src/channels/plugins/registry.ts create mode 100644 src/channels/plugins/target-parsing.ts create mode 100644 src/plugin-sdk/routing.ts create mode 100644 src/plugins/runtime/runtime-discord.ts create mode 100644 src/plugins/runtime/runtime-imessage.ts create mode 100644 src/plugins/runtime/runtime-signal.ts create mode 100644 src/plugins/runtime/runtime-slack.ts create mode 100644 src/plugins/runtime/runtime-telegram.ts diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 88295c61880..e3833f56ed5 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -41,9 +41,17 @@ import { type OpenClawConfig, type ResolvedDiscordAccount, } from "openclaw/plugin-sdk/discord"; +import { + buildAgentSessionKey, + resolveThreadSessionKeys, + type RoutePeer, +} from "openclaw/plugin-sdk/routing"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import { normalizeMessageChannel } from "../../../src/utils/message-channel.js"; -import { isDiscordExecApprovalClientEnabled } from "./exec-approvals.js"; +import { + isDiscordExecApprovalClientEnabled, + shouldSuppressLocalDiscordExecApprovalPrompt, +} from "./exec-approvals.js"; import type { DiscordProbe } from "./probe.js"; import { resolveDiscordUserAllowlist } from "./resolve-users.js"; import { getDiscordRuntime } from "./runtime.js"; @@ -453,6 +461,12 @@ export const discordPlugin: ChannelPlugin = { isDiscordExecApprovalClientEnabled({ cfg, accountId }) ? { kind: "enabled" } : { kind: "disabled" }, + shouldSuppressLocalPrompt: ({ cfg, accountId, payload }) => + shouldSuppressLocalDiscordExecApprovalPrompt({ + cfg, + accountId, + payload, + }), hasConfiguredDmRoute: ({ cfg }) => hasDiscordExecApprovalDmRoute(cfg), shouldSuppressForwardingFallback: ({ cfg, target }) => (normalizeMessageChannel(target.channel) ?? target.channel) === "discord" && diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index 2d5b0ba9b39..e66ee5a3c82 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -27,6 +27,7 @@ import { type ChannelPlugin, type ResolvedIMessageAccount, } from "openclaw/plugin-sdk/imessage"; +import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js"; import { getIMessageRuntime } from "./runtime.js"; diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index bb65502e017..a38db8b60d5 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -4,7 +4,7 @@ import { createScopedAccountConfigAccessors, collectAllowlistProviderRestrictSendersWarnings, } from "openclaw/plugin-sdk/compat"; -import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core"; +import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary, diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 3b0e347ba24..592ce5a6331 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -11,7 +11,7 @@ import { buildAgentSessionKey, resolveThreadSessionKeys, type RoutePeer, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/routing"; import { buildComputedAccountStatusSnapshot, buildChannelConfigSchema, diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 37915fa07df..0b7da81d561 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -11,7 +11,7 @@ import { buildAgentSessionKey, resolveThreadSessionKeys, type RoutePeer, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/routing"; import { buildChannelConfigSchema, buildTokenChannelStatusSummary, diff --git a/package.json b/package.json index 5aeb794f174..0a345f172a0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,10 @@ "types": "./dist/plugin-sdk/compat.d.ts", "default": "./dist/plugin-sdk/compat.js" }, + "./plugin-sdk/routing": { + "types": "./dist/plugin-sdk/routing.d.ts", + "default": "./dist/plugin-sdk/routing.js" + }, "./plugin-sdk/telegram": { "types": "./dist/plugin-sdk/telegram.d.ts", "default": "./dist/plugin-sdk/telegram.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index c42f27db5a1..91b16c36450 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -2,6 +2,7 @@ "index", "core", "compat", + "routing", "telegram", "discord", "slack", diff --git a/src/auto-reply/reply/commands-subagents/shared.ts b/src/auto-reply/reply/commands-subagents/shared.ts index 1c7db7e13cd..9781683267e 100644 --- a/src/auto-reply/reply/commands-subagents/shared.ts +++ b/src/auto-reply/reply/commands-subagents/shared.ts @@ -1,4 +1,3 @@ -import { parseDiscordTarget } from "../../../../extensions/discord/src/targets.js"; import { resolveStoredSubagentCapabilities } from "../../../agents/subagent-capabilities.js"; import type { ResolvedSubagentController } from "../../../agents/subagent-control.js"; import { @@ -12,6 +11,7 @@ import { sanitizeTextContent, stripToolMessages, } from "../../../agents/tools/sessions-helpers.js"; +import { parseExplicitTargetForChannel } from "../../../channels/plugins/target-parsing.js"; import type { SessionEntry, loadSessionStore as loadSessionStoreFn, @@ -335,13 +335,9 @@ export function resolveDiscordChannelIdForFocus( typeof params.ctx.To === "string" ? params.ctx.To.trim() : "", ].filter(Boolean); for (const candidate of toCandidates) { - try { - const target = parseDiscordTarget(candidate, { defaultKind: "channel" }); - if (target?.kind === "channel" && target.id) { - return target.id; - } - } catch { - // Ignore parse failures and try the next candidate. + const target = parseExplicitTargetForChannel("discord", candidate); + if (target?.chatType === "channel" && target.to) { + return target.to; } } return undefined; diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 38e3615dd9f..6d1604227bd 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -1,8 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { discordPlugin } from "../../../extensions/discord/src/channel.js"; import { AcpRuntimeError } from "../../acp/runtime/errors.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js"; import type { PluginTargetedInboundClaimOutcome } from "../../plugins/hooks.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; import type { MsgContext } from "../templating.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; @@ -252,6 +255,9 @@ async function dispatchTwiceWithFreshDispatchers(params: Omit { beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([{ pluginId: "discord", source: "test", plugin: discordPlugin }]), + ); acpManagerTesting.resetAcpSessionManagerForTests(); resetInboundDedupe(); mocks.routeReply.mockReset(); @@ -1295,6 +1301,11 @@ describe("dispatchReplyFromConfig", () => { commands: { text: false, }, + session: { + sendPolicy: { + default: "allow", + }, + }, } as OpenClawConfig; const dispatcher = createDispatcher(); const ctx = buildTestCtx({ diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 1e90dd58887..18a7eb7802d 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -1,5 +1,5 @@ -import { shouldSuppressLocalDiscordExecApprovalPrompt } from "../../../extensions/discord/src/exec-approvals.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; +import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js"; import type { OpenClawConfig } from "../../config/config.js"; import { loadSessionStore, @@ -506,8 +506,8 @@ export async function dispatchReplyFromConfig(params: { const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => { if ( - normalizeMessageChannel(ctx.Surface ?? ctx.Provider) === "discord" && - shouldSuppressLocalDiscordExecApprovalPrompt({ + shouldSuppressLocalExecApprovalPrompt({ + channel: normalizeMessageChannel(ctx.Surface ?? ctx.Provider), cfg, accountId: ctx.AccountId, payload, diff --git a/src/auto-reply/reply/reply-payloads.ts b/src/auto-reply/reply/reply-payloads.ts index f5f409e2900..7d7ae82975c 100644 --- a/src/auto-reply/reply/reply-payloads.ts +++ b/src/auto-reply/reply/reply-payloads.ts @@ -1,7 +1,7 @@ -import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js"; import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js"; import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js"; import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { parseExplicitTargetForChannel } from "../../channels/plugins/target-parsing.js"; import type { ReplyToMode } from "../../config/types.js"; import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js"; import { hasReplyChannelData, hasReplyContent } from "../../interactive/payload.js"; @@ -210,15 +210,16 @@ function targetsMatchForSuppression(params: { return params.targetKey === params.originTarget; } - const origin = parseTelegramTarget(params.originTarget); - const target = parseTelegramTarget(params.targetKey); + const origin = parseExplicitTargetForChannel("telegram", params.originTarget); + const target = parseExplicitTargetForChannel("telegram", params.targetKey); + if (!origin || !target) { + return params.targetKey === params.originTarget; + } const explicitTargetThreadId = normalizeThreadIdForComparison(params.targetThreadId); const targetThreadId = - explicitTargetThreadId ?? - (target.messageThreadId != null ? String(target.messageThreadId) : undefined); - const originThreadId = - origin.messageThreadId != null ? String(origin.messageThreadId) : undefined; - if (origin.chatId.trim().toLowerCase() !== target.chatId.trim().toLowerCase()) { + explicitTargetThreadId ?? (target.threadId != null ? String(target.threadId) : undefined); + const originThreadId = origin.threadId != null ? String(origin.threadId) : undefined; + if (origin.to.trim().toLowerCase() !== target.to.trim().toLowerCase()) { return false; } if (originThreadId && targetThreadId != null) { diff --git a/src/auto-reply/reply/telegram-context.test.ts b/src/auto-reply/reply/telegram-context.test.ts index 7b58b780180..b38397a1c01 100644 --- a/src/auto-reply/reply/telegram-context.test.ts +++ b/src/auto-reply/reply/telegram-context.test.ts @@ -1,6 +1,15 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import { telegramPlugin } from "../../../extensions/telegram/src/channel.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { resolveTelegramConversationId } from "./telegram-context.js"; +beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([{ pluginId: "telegram", source: "test", plugin: telegramPlugin }]), + ); +}); + describe("resolveTelegramConversationId", () => { it("builds canonical topic ids from chat target and message thread id", () => { const conversationId = resolveTelegramConversationId({ diff --git a/src/auto-reply/reply/telegram-context.ts b/src/auto-reply/reply/telegram-context.ts index f209af031fb..8ab905a44d1 100644 --- a/src/auto-reply/reply/telegram-context.ts +++ b/src/auto-reply/reply/telegram-context.ts @@ -1,4 +1,4 @@ -import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js"; +import { parseExplicitTargetForChannel } from "../../channels/plugins/target-parsing.js"; type TelegramConversationParams = { ctx: { @@ -25,7 +25,7 @@ export function resolveTelegramConversationId( .map((value) => value.trim()) .filter(Boolean); const chatId = toCandidates - .map((candidate) => parseTelegramTarget(candidate).chatId.trim()) + .map((candidate) => parseExplicitTargetForChannel("telegram", candidate)?.to.trim() ?? "") .find((candidate) => candidate.length > 0); if (!chatId) { return undefined; diff --git a/src/channels/plugins/directory-config.ts b/src/channels/plugins/directory-config.ts index 45fb8bcf46a..e54889076e8 100644 --- a/src/channels/plugins/directory-config.ts +++ b/src/channels/plugins/directory-config.ts @@ -1,12 +1,13 @@ -import { inspectDiscordAccount } from "../../../extensions/discord/src/account-inspect.js"; -import { inspectSlackAccount } from "../../../extensions/slack/src/account-inspect.js"; -import { inspectTelegramAccount } from "../../../extensions/telegram/src/account-inspect.js"; -import { resolveWhatsAppAccount } from "../../../extensions/whatsapp/src/accounts.js"; import type { OpenClawConfig } from "../../config/types.js"; import { mapAllowFromEntries } from "../../plugin-sdk/channel-config-helpers.js"; import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; +import type { InspectedDiscordAccount } from "../read-only-account-inspect.discord.runtime.js"; +import { inspectReadOnlyChannelAccount } from "../read-only-account-inspect.js"; +import type { InspectedSlackAccount } from "../read-only-account-inspect.slack.runtime.js"; +import type { InspectedTelegramAccount } from "../read-only-account-inspect.telegram.runtime.js"; import { applyDirectoryQueryAndLimit, toDirectoryEntries } from "./directory-config-helpers.js"; import { normalizeSlackMessagingTarget } from "./normalize/slack.js"; +import { getChannelPlugin } from "./registry.js"; import type { ChannelDirectoryEntry } from "./types.js"; export type DirectoryConfigParams = { @@ -58,7 +59,14 @@ function normalizeTrimmedSet( export async function listSlackDirectoryPeersFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectSlackAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "slack", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedSlackAccount | null; + if (!account || !("config" in account)) { + return []; + } const ids = new Set(); addAllowFromAndDmsIds(ids, account.config.allowFrom ?? account.dm?.allowFrom, account.config.dms); @@ -81,7 +89,14 @@ export async function listSlackDirectoryPeersFromConfig( export async function listSlackDirectoryGroupsFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectSlackAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "slack", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedSlackAccount | null; + if (!account || !("config" in account)) { + return []; + } const ids = Object.keys(account.config.channels ?? {}) .map((raw) => raw.trim()) .filter(Boolean) @@ -93,7 +108,14 @@ export async function listSlackDirectoryGroupsFromConfig( export async function listDiscordDirectoryPeersFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectDiscordAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } const ids = new Set(); addAllowFromAndDmsIds( @@ -122,7 +144,14 @@ export async function listDiscordDirectoryPeersFromConfig( export async function listDiscordDirectoryGroupsFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectDiscordAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } const ids = new Set(); for (const guild of Object.values(account.config.guilds ?? {})) { addTrimmedEntries(ids, Object.keys(guild.channels ?? {})); @@ -142,7 +171,14 @@ export async function listDiscordDirectoryGroupsFromConfig( export async function listTelegramDirectoryPeersFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "telegram", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedTelegramAccount | null; + if (!account || !("config" in account)) { + return []; + } const raw = [ ...mapAllowFromEntries(account.config.allowFrom), ...Object.keys(account.config.dms ?? {}), @@ -173,7 +209,14 @@ export async function listTelegramDirectoryPeersFromConfig( export async function listTelegramDirectoryGroupsFromConfig( params: DirectoryConfigParams, ): Promise { - const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = (await inspectReadOnlyChannelAccount({ + channelId: "telegram", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedTelegramAccount | null; + if (!account || !("config" in account)) { + return []; + } const ids = Object.keys(account.config.groups ?? {}) .map((id) => id.trim()) .filter((id) => Boolean(id) && id !== "*"); @@ -183,9 +226,15 @@ export async function listTelegramDirectoryGroupsFromConfig( export async function listWhatsAppDirectoryPeersFromConfig( params: DirectoryConfigParams, ): Promise { - const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = getChannelPlugin("whatsapp")?.config.resolveAccount( + params.cfg, + params.accountId, + ) as { allowFrom?: unknown[] } | null | undefined; + if (!account || typeof account !== "object") { + return []; + } const ids = (account.allowFrom ?? []) - .map((entry) => String(entry).trim()) + .map((entry: unknown) => String(entry).trim()) .filter((entry) => Boolean(entry) && entry !== "*") .map((entry) => normalizeWhatsAppTarget(entry) ?? "") .filter(Boolean) @@ -196,7 +245,13 @@ export async function listWhatsAppDirectoryPeersFromConfig( export async function listWhatsAppDirectoryGroupsFromConfig( params: DirectoryConfigParams, ): Promise { - const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = getChannelPlugin("whatsapp")?.config.resolveAccount( + params.cfg, + params.accountId, + ) as { groups?: Record } | null | undefined; + if (!account || typeof account !== "object") { + return []; + } const ids = Object.keys(account.groups ?? {}) .map((id) => id.trim()) .filter((id) => Boolean(id) && id !== "*"); diff --git a/src/channels/plugins/exec-approval-local.ts b/src/channels/plugins/exec-approval-local.ts new file mode 100644 index 00000000000..0f4f409e0ae --- /dev/null +++ b/src/channels/plugins/exec-approval-local.ts @@ -0,0 +1,22 @@ +import type { ReplyPayload } from "../../auto-reply/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { getChannelPlugin, normalizeChannelId } from "./registry.js"; + +export function shouldSuppressLocalExecApprovalPrompt(params: { + channel?: string | null; + cfg: OpenClawConfig; + accountId?: string | null; + payload: ReplyPayload; +}): boolean { + const channel = params.channel ? normalizeChannelId(params.channel) : null; + if (!channel) { + return false; + } + return ( + getChannelPlugin(channel)?.execApprovals?.shouldSuppressLocalPrompt?.({ + cfg: params.cfg, + accountId: params.accountId, + payload: params.payload, + }) ?? false + ); +} diff --git a/src/channels/plugins/index.ts b/src/channels/plugins/index.ts index f2a8aa56b95..d7c91248109 100644 --- a/src/channels/plugins/index.ts +++ b/src/channels/plugins/index.ts @@ -1,93 +1,4 @@ -import { - getActivePluginRegistryVersion, - requireActivePluginRegistry, -} from "../../plugins/runtime.js"; -import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js"; -import type { ChannelId, ChannelPlugin } from "./types.js"; - -// Channel plugins registry (runtime). -// -// This module is intentionally "heavy" (plugins may import channel monitors, web login, etc). -// Shared code paths should prefer narrower adapters and helpers instead of reaching into -// channel-specific runtime modules directly. -// -function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] { - const seen = new Set(); - const resolved: ChannelPlugin[] = []; - for (const plugin of channels) { - const id = String(plugin.id).trim(); - if (!id || seen.has(id)) { - continue; - } - seen.add(id); - resolved.push(plugin); - } - return resolved; -} - -type CachedChannelPlugins = { - registryVersion: number; - sorted: ChannelPlugin[]; - byId: Map; -}; - -const EMPTY_CHANNEL_PLUGIN_CACHE: CachedChannelPlugins = { - registryVersion: -1, - sorted: [], - byId: new Map(), -}; - -let cachedChannelPlugins = EMPTY_CHANNEL_PLUGIN_CACHE; - -function resolveCachedChannelPlugins(): CachedChannelPlugins { - const registry = requireActivePluginRegistry(); - const registryVersion = getActivePluginRegistryVersion(); - const cached = cachedChannelPlugins; - if (cached.registryVersion === registryVersion) { - return cached; - } - - const sorted = dedupeChannels(registry.channels.map((entry) => entry.plugin)).toSorted((a, b) => { - const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId); - const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId); - const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); - const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); - if (orderA !== orderB) { - return orderA - orderB; - } - return a.id.localeCompare(b.id); - }); - const byId = new Map(); - for (const plugin of sorted) { - byId.set(plugin.id, plugin); - } - - const next: CachedChannelPlugins = { - registryVersion, - sorted, - byId, - }; - cachedChannelPlugins = next; - return next; -} - -export function listChannelPlugins(): ChannelPlugin[] { - return resolveCachedChannelPlugins().sorted.slice(); -} - -export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined { - const resolvedId = String(id).trim(); - if (!resolvedId) { - return undefined; - } - return resolveCachedChannelPlugins().byId.get(resolvedId); -} - -export function normalizeChannelId(raw?: string | null): ChannelId | null { - // Channel docking: keep input normalization centralized in src/channels/registry.ts. - // Plugin registry must be initialized before calling. - return normalizeAnyChannelId(raw); -} +export { getChannelPlugin, listChannelPlugins, normalizeChannelId } from "./registry.js"; export { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig, diff --git a/src/channels/plugins/registry.ts b/src/channels/plugins/registry.ts new file mode 100644 index 00000000000..3f170cbfb92 --- /dev/null +++ b/src/channels/plugins/registry.ts @@ -0,0 +1,82 @@ +import { + getActivePluginRegistryVersion, + requireActivePluginRegistry, +} from "../../plugins/runtime.js"; +import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js"; +import type { ChannelId, ChannelPlugin } from "./types.js"; + +function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] { + const seen = new Set(); + const resolved: ChannelPlugin[] = []; + for (const plugin of channels) { + const id = String(plugin.id).trim(); + if (!id || seen.has(id)) { + continue; + } + seen.add(id); + resolved.push(plugin); + } + return resolved; +} + +type CachedChannelPlugins = { + registryVersion: number; + sorted: ChannelPlugin[]; + byId: Map; +}; + +const EMPTY_CHANNEL_PLUGIN_CACHE: CachedChannelPlugins = { + registryVersion: -1, + sorted: [], + byId: new Map(), +}; + +let cachedChannelPlugins = EMPTY_CHANNEL_PLUGIN_CACHE; + +function resolveCachedChannelPlugins(): CachedChannelPlugins { + const registry = requireActivePluginRegistry(); + const registryVersion = getActivePluginRegistryVersion(); + const cached = cachedChannelPlugins; + if (cached.registryVersion === registryVersion) { + return cached; + } + + const sorted = dedupeChannels(registry.channels.map((entry) => entry.plugin)).toSorted((a, b) => { + const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId); + const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId); + const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); + const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); + if (orderA !== orderB) { + return orderA - orderB; + } + return a.id.localeCompare(b.id); + }); + const byId = new Map(); + for (const plugin of sorted) { + byId.set(plugin.id, plugin); + } + + const next: CachedChannelPlugins = { + registryVersion, + sorted, + byId, + }; + cachedChannelPlugins = next; + return next; +} + +export function listChannelPlugins(): ChannelPlugin[] { + return resolveCachedChannelPlugins().sorted.slice(); +} + +export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined { + const resolvedId = String(id).trim(); + if (!resolvedId) { + return undefined; + } + return resolveCachedChannelPlugins().byId.get(resolvedId); +} + +export function normalizeChannelId(raw?: string | null): ChannelId | null { + return normalizeAnyChannelId(raw); +} diff --git a/src/channels/plugins/target-parsing.ts b/src/channels/plugins/target-parsing.ts new file mode 100644 index 00000000000..5d7fd6e28da --- /dev/null +++ b/src/channels/plugins/target-parsing.ts @@ -0,0 +1,26 @@ +import type { ChatType } from "../chat-type.js"; +import { getChannelPlugin, normalizeChannelId } from "./registry.js"; + +export type ParsedChannelExplicitTarget = { + to: string; + threadId?: string | number; + chatType?: ChatType; +}; + +function parseWithPlugin( + rawChannel: string, + rawTarget: string, +): ParsedChannelExplicitTarget | null { + const channel = normalizeChannelId(rawChannel); + if (!channel) { + return null; + } + return getChannelPlugin(channel)?.messaging?.parseExplicitTarget?.({ raw: rawTarget }) ?? null; +} + +export function parseExplicitTargetForChannel( + channel: string, + rawTarget: string, +): ParsedChannelExplicitTarget | null { + return parseWithPlugin(channel, rawTarget); +} diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index 9f9e279bdc1..eff6878e85e 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -456,6 +456,11 @@ export type ChannelExecApprovalAdapter = { cfg: OpenClawConfig; accountId?: string | null; }) => ChannelExecApprovalInitiatingSurfaceState; + shouldSuppressLocalPrompt?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + payload: ReplyPayload; + }) => boolean; hasConfiguredDmRoute?: (params: { cfg: OpenClawConfig }) => boolean; shouldSuppressForwardingFallback?: (params: { cfg: OpenClawConfig; diff --git a/src/plugin-sdk/channel-config-helpers.ts b/src/plugin-sdk/channel-config-helpers.ts index ccd5de7a31a..aa405c4b9b7 100644 --- a/src/plugin-sdk/channel-config-helpers.ts +++ b/src/plugin-sdk/channel-config-helpers.ts @@ -1,11 +1,10 @@ -import { resolveIMessageAccount } from "../../extensions/imessage/src/accounts.js"; -import { resolveWhatsAppAccount } from "../../extensions/whatsapp/src/accounts.js"; import { deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "../channels/plugins/config-helpers.js"; import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js"; import { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js"; +import { getChannelPlugin } from "../channels/plugins/registry.js"; import type { ChannelConfigAdapter } from "../channels/plugins/types.adapters.js"; import type { OpenClawConfig } from "../config/config.js"; import { normalizeAccountId } from "../routing/session-key.js"; @@ -148,7 +147,10 @@ export function resolveWhatsAppConfigAllowFrom(params: { cfg: OpenClawConfig; accountId?: string | null; }): string[] { - return resolveWhatsAppAccount(params).allowFrom ?? []; + const account = getChannelPlugin("whatsapp")?.config.resolveAccount(params.cfg, params.accountId); + return account && typeof account === "object" && Array.isArray(account.allowFrom) + ? account.allowFrom.map(String) + : []; } export function formatWhatsAppConfigAllowFromEntries(allowFrom: Array): string[] { @@ -169,12 +171,20 @@ export function resolveIMessageConfigAllowFrom(params: { cfg: OpenClawConfig; accountId?: string | null; }): string[] { - return mapAllowFromEntries(resolveIMessageAccount(params).config.allowFrom); + const account = getChannelPlugin("imessage")?.config.resolveAccount(params.cfg, params.accountId); + if (!account || typeof account !== "object" || !("config" in account)) { + return []; + } + return mapAllowFromEntries(account.config.allowFrom); } export function resolveIMessageConfigDefaultTo(params: { cfg: OpenClawConfig; accountId?: string | null; }): string | undefined { - return resolveOptionalConfigString(resolveIMessageAccount(params).config.defaultTo); + const account = getChannelPlugin("imessage")?.config.resolveAccount(params.cfg, params.accountId); + if (!account || typeof account !== "object" || !("config" in account)) { + return undefined; + } + return resolveOptionalConfigString(account.config.defaultTo); } diff --git a/src/plugin-sdk/routing.ts b/src/plugin-sdk/routing.ts new file mode 100644 index 00000000000..921d085ae55 --- /dev/null +++ b/src/plugin-sdk/routing.ts @@ -0,0 +1,6 @@ +export { + buildAgentSessionKey, + type RoutePeer, + type RoutePeerKind, +} from "../routing/resolve-route.js"; +export { resolveThreadSessionKeys } from "../routing/session-key.js"; diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 64f953fb014..6f371305a81 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -1,4 +1,6 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { discordPlugin } from "../../extensions/discord/src/channel.js"; +import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { __testing, clearPluginCommands, @@ -7,6 +9,13 @@ import { listPluginCommands, registerPluginCommand, } from "./commands.js"; +import { setActivePluginRegistry } from "./runtime.js"; + +beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([{ pluginId: "discord", source: "test", plugin: discordPlugin }]), + ); +}); afterEach(() => { clearPluginCommands(); diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index 91e38a6ae99..fdd71d4f31c 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -5,8 +5,7 @@ * These commands are processed before built-in commands and before agent invocation. */ -import { parseDiscordTarget } from "../../extensions/discord/src/targets.js"; -import { parseTelegramTarget } from "../../extensions/telegram/src/targets.js"; +import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js"; import type { OpenClawConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; import { @@ -286,12 +285,15 @@ function resolveBindingConversationFromCommand(params: { if (!rawTarget) { return null; } - const target = parseTelegramTarget(rawTarget); + const target = parseExplicitTargetForChannel("telegram", rawTarget); + if (!target) { + return null; + } return { channel: "telegram", accountId, - conversationId: target.chatId, - threadId: params.messageThreadId ?? target.messageThreadId, + conversationId: target.to, + threadId: params.messageThreadId ?? target.threadId, }; } if (params.channel === "discord") { @@ -304,14 +306,14 @@ function resolveBindingConversationFromCommand(params: { if (!rawTarget || rawTarget.startsWith("slash:")) { return null; } - const target = parseDiscordTarget(rawTarget, { defaultKind: "channel" }); + const target = parseExplicitTargetForChannel("discord", rawTarget); if (!target) { return null; } return { channel: "discord", accountId, - conversationId: `${target.kind}:${target.id}`, + conversationId: `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`, }; } return null; diff --git a/src/plugins/runtime/runtime-channel.ts b/src/plugins/runtime/runtime-channel.ts index 94ea9a0b8cb..23b47d48eeb 100644 --- a/src/plugins/runtime/runtime-channel.ts +++ b/src/plugins/runtime/runtime-channel.ts @@ -1,59 +1,4 @@ -import { auditDiscordChannelPermissions } from "../../../extensions/discord/src/audit.js"; -import { - listDiscordDirectoryGroupsLive, - listDiscordDirectoryPeersLive, -} from "../../../extensions/discord/src/directory-live.js"; -import { monitorDiscordProvider } from "../../../extensions/discord/src/monitor.js"; -import { probeDiscord } from "../../../extensions/discord/src/probe.js"; -import { resolveDiscordChannelAllowlist } from "../../../extensions/discord/src/resolve-channels.js"; -import { resolveDiscordUserAllowlist } from "../../../extensions/discord/src/resolve-users.js"; -import { - createThreadDiscord, - deleteMessageDiscord, - editChannelDiscord, - editMessageDiscord, - pinMessageDiscord, - sendDiscordComponentMessage, - sendMessageDiscord, - sendPollDiscord, - sendTypingDiscord, - unpinMessageDiscord, -} from "../../../extensions/discord/src/send.js"; -import { monitorIMessageProvider } from "../../../extensions/imessage/src/monitor.js"; -import { probeIMessage } from "../../../extensions/imessage/src/probe.js"; -import { sendMessageIMessage } from "../../../extensions/imessage/src/send.js"; -import { monitorSignalProvider } from "../../../extensions/signal/src/index.js"; -import { probeSignal } from "../../../extensions/signal/src/probe.js"; -import { sendMessageSignal } from "../../../extensions/signal/src/send.js"; -import { - listSlackDirectoryGroupsLive, - listSlackDirectoryPeersLive, -} from "../../../extensions/slack/src/directory-live.js"; -import { monitorSlackProvider } from "../../../extensions/slack/src/index.js"; -import { probeSlack } from "../../../extensions/slack/src/probe.js"; -import { resolveSlackChannelAllowlist } from "../../../extensions/slack/src/resolve-channels.js"; -import { resolveSlackUserAllowlist } from "../../../extensions/slack/src/resolve-users.js"; -import { sendMessageSlack } from "../../../extensions/slack/src/send.js"; -import { - auditTelegramGroupMembership, - collectTelegramUnmentionedGroupIds, -} from "../../../extensions/telegram/src/audit.js"; -import { monitorTelegramProvider } from "../../../extensions/telegram/src/monitor.js"; -import { probeTelegram } from "../../../extensions/telegram/src/probe.js"; -import { - deleteMessageTelegram, - editMessageReplyMarkupTelegram, - editMessageTelegram, - pinMessageTelegram, - renameForumTopicTelegram, - sendMessageTelegram, - sendPollTelegram, - sendTypingTelegram, - unpinMessageTelegram, -} from "../../../extensions/telegram/src/send.js"; -import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js"; import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; -import { handleSlackAction } from "../../agents/tools/slack-actions.js"; import { chunkByNewline, chunkMarkdownText, @@ -90,9 +35,6 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; -import { discordMessageActions } from "../../channels/plugins/actions/discord.js"; -import { signalMessageActions } from "../../channels/plugins/actions/signal.js"; -import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js"; import { recordInboundSession } from "../../channels/session.js"; import { resolveChannelGroupPolicy, @@ -134,8 +76,11 @@ import { upsertChannelPairingRequest, } from "../../pairing/pairing-store.js"; import { buildAgentSessionKey, resolveAgentRoute } from "../../routing/resolve-route.js"; -import { createDiscordTypingLease } from "./runtime-discord-typing.js"; -import { createTelegramTypingLease } from "./runtime-telegram-typing.js"; +import { createRuntimeDiscord } from "./runtime-discord.js"; +import { createRuntimeIMessage } from "./runtime-imessage.js"; +import { createRuntimeSignal } from "./runtime-signal.js"; +import { createRuntimeSlack } from "./runtime-slack.js"; +import { createRuntimeTelegram } from "./runtime-telegram.js"; import { createRuntimeWhatsApp } from "./runtime-whatsapp.js"; import type { PluginRuntime } from "./types.js"; @@ -222,100 +167,11 @@ export function createRuntimeChannel(): PluginRuntime["channel"] { shouldComputeCommandAuthorized, shouldHandleTextCommands, }, - discord: { - messageActions: discordMessageActions, - auditChannelPermissions: auditDiscordChannelPermissions, - listDirectoryGroupsLive: listDiscordDirectoryGroupsLive, - listDirectoryPeersLive: listDiscordDirectoryPeersLive, - probeDiscord, - resolveChannelAllowlist: resolveDiscordChannelAllowlist, - resolveUserAllowlist: resolveDiscordUserAllowlist, - sendComponentMessage: sendDiscordComponentMessage, - sendMessageDiscord, - sendPollDiscord, - monitorDiscordProvider, - typing: { - pulse: sendTypingDiscord, - start: async ({ channelId, accountId, cfg, intervalMs }) => - await createDiscordTypingLease({ - channelId, - accountId, - cfg, - intervalMs, - pulse: async ({ channelId, accountId, cfg }) => - void (await sendTypingDiscord(channelId, { - accountId, - cfg, - })), - }), - }, - conversationActions: { - editMessage: editMessageDiscord, - deleteMessage: deleteMessageDiscord, - pinMessage: pinMessageDiscord, - unpinMessage: unpinMessageDiscord, - createThread: createThreadDiscord, - editChannel: editChannelDiscord, - }, - }, - slack: { - listDirectoryGroupsLive: listSlackDirectoryGroupsLive, - listDirectoryPeersLive: listSlackDirectoryPeersLive, - probeSlack, - resolveChannelAllowlist: resolveSlackChannelAllowlist, - resolveUserAllowlist: resolveSlackUserAllowlist, - sendMessageSlack, - monitorSlackProvider, - handleSlackAction, - }, - telegram: { - auditGroupMembership: auditTelegramGroupMembership, - collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds, - probeTelegram, - resolveTelegramToken, - sendMessageTelegram, - sendPollTelegram, - monitorTelegramProvider, - messageActions: telegramMessageActions, - typing: { - pulse: sendTypingTelegram, - start: async ({ to, accountId, cfg, intervalMs, messageThreadId }) => - await createTelegramTypingLease({ - to, - accountId, - cfg, - intervalMs, - messageThreadId, - pulse: async ({ to, accountId, cfg, messageThreadId }) => - await sendTypingTelegram(to, { - accountId, - cfg, - messageThreadId, - }), - }), - }, - conversationActions: { - editMessage: editMessageTelegram, - editReplyMarkup: editMessageReplyMarkupTelegram, - clearReplyMarkup: async (chatIdInput, messageIdInput, opts = {}) => - await editMessageReplyMarkupTelegram(chatIdInput, messageIdInput, [], opts), - deleteMessage: deleteMessageTelegram, - renameTopic: renameForumTopicTelegram, - pinMessage: pinMessageTelegram, - unpinMessage: unpinMessageTelegram, - }, - }, - signal: { - probeSignal, - sendMessageSignal, - monitorSignalProvider, - messageActions: signalMessageActions, - }, - imessage: { - monitorIMessageProvider, - probeIMessage, - sendMessageIMessage, - }, + discord: createRuntimeDiscord(), + slack: createRuntimeSlack(), + telegram: createRuntimeTelegram(), + signal: createRuntimeSignal(), + imessage: createRuntimeIMessage(), whatsapp: createRuntimeWhatsApp(), line: { listLineAccountIds, diff --git a/src/plugins/runtime/runtime-discord.ts b/src/plugins/runtime/runtime-discord.ts new file mode 100644 index 00000000000..6f827f27504 --- /dev/null +++ b/src/plugins/runtime/runtime-discord.ts @@ -0,0 +1,60 @@ +import { auditDiscordChannelPermissions } from "../../../extensions/discord/src/audit.js"; +import { + listDiscordDirectoryGroupsLive, + listDiscordDirectoryPeersLive, +} from "../../../extensions/discord/src/directory-live.js"; +import { monitorDiscordProvider } from "../../../extensions/discord/src/monitor.js"; +import { probeDiscord } from "../../../extensions/discord/src/probe.js"; +import { resolveDiscordChannelAllowlist } from "../../../extensions/discord/src/resolve-channels.js"; +import { resolveDiscordUserAllowlist } from "../../../extensions/discord/src/resolve-users.js"; +import { + createThreadDiscord, + deleteMessageDiscord, + editChannelDiscord, + editMessageDiscord, + pinMessageDiscord, + sendDiscordComponentMessage, + sendMessageDiscord, + sendPollDiscord, + sendTypingDiscord, + unpinMessageDiscord, +} from "../../../extensions/discord/src/send.js"; +import { discordMessageActions } from "../../channels/plugins/actions/discord.js"; +import { createDiscordTypingLease } from "./runtime-discord-typing.js"; +import type { PluginRuntimeChannel } from "./types-channel.js"; + +export function createRuntimeDiscord(): PluginRuntimeChannel["discord"] { + return { + messageActions: discordMessageActions, + auditChannelPermissions: auditDiscordChannelPermissions, + listDirectoryGroupsLive: listDiscordDirectoryGroupsLive, + listDirectoryPeersLive: listDiscordDirectoryPeersLive, + probeDiscord, + resolveChannelAllowlist: resolveDiscordChannelAllowlist, + resolveUserAllowlist: resolveDiscordUserAllowlist, + sendComponentMessage: sendDiscordComponentMessage, + sendMessageDiscord, + sendPollDiscord, + monitorDiscordProvider, + typing: { + pulse: sendTypingDiscord, + start: async ({ channelId, accountId, cfg, intervalMs }) => + await createDiscordTypingLease({ + channelId, + accountId, + cfg, + intervalMs, + pulse: async ({ channelId, accountId, cfg }) => + void (await sendTypingDiscord(channelId, { accountId, cfg })), + }), + }, + conversationActions: { + editMessage: editMessageDiscord, + deleteMessage: deleteMessageDiscord, + pinMessage: pinMessageDiscord, + unpinMessage: unpinMessageDiscord, + createThread: createThreadDiscord, + editChannel: editChannelDiscord, + }, + }; +} diff --git a/src/plugins/runtime/runtime-imessage.ts b/src/plugins/runtime/runtime-imessage.ts new file mode 100644 index 00000000000..01430cacc3c --- /dev/null +++ b/src/plugins/runtime/runtime-imessage.ts @@ -0,0 +1,12 @@ +import { monitorIMessageProvider } from "../../../extensions/imessage/src/monitor.js"; +import { probeIMessage } from "../../../extensions/imessage/src/probe.js"; +import { sendMessageIMessage } from "../../../extensions/imessage/src/send.js"; +import type { PluginRuntimeChannel } from "./types-channel.js"; + +export function createRuntimeIMessage(): PluginRuntimeChannel["imessage"] { + return { + monitorIMessageProvider, + probeIMessage, + sendMessageIMessage, + }; +} diff --git a/src/plugins/runtime/runtime-signal.ts b/src/plugins/runtime/runtime-signal.ts new file mode 100644 index 00000000000..2465ecbdbbc --- /dev/null +++ b/src/plugins/runtime/runtime-signal.ts @@ -0,0 +1,14 @@ +import { monitorSignalProvider } from "../../../extensions/signal/src/index.js"; +import { probeSignal } from "../../../extensions/signal/src/probe.js"; +import { sendMessageSignal } from "../../../extensions/signal/src/send.js"; +import { signalMessageActions } from "../../channels/plugins/actions/signal.js"; +import type { PluginRuntimeChannel } from "./types-channel.js"; + +export function createRuntimeSignal(): PluginRuntimeChannel["signal"] { + return { + probeSignal, + sendMessageSignal, + monitorSignalProvider, + messageActions: signalMessageActions, + }; +} diff --git a/src/plugins/runtime/runtime-slack.ts b/src/plugins/runtime/runtime-slack.ts new file mode 100644 index 00000000000..095b14ec9c7 --- /dev/null +++ b/src/plugins/runtime/runtime-slack.ts @@ -0,0 +1,24 @@ +import { + listSlackDirectoryGroupsLive, + listSlackDirectoryPeersLive, +} from "../../../extensions/slack/src/directory-live.js"; +import { monitorSlackProvider } from "../../../extensions/slack/src/index.js"; +import { probeSlack } from "../../../extensions/slack/src/probe.js"; +import { resolveSlackChannelAllowlist } from "../../../extensions/slack/src/resolve-channels.js"; +import { resolveSlackUserAllowlist } from "../../../extensions/slack/src/resolve-users.js"; +import { sendMessageSlack } from "../../../extensions/slack/src/send.js"; +import { handleSlackAction } from "../../agents/tools/slack-actions.js"; +import type { PluginRuntimeChannel } from "./types-channel.js"; + +export function createRuntimeSlack(): PluginRuntimeChannel["slack"] { + return { + listDirectoryGroupsLive: listSlackDirectoryGroupsLive, + listDirectoryPeersLive: listSlackDirectoryPeersLive, + probeSlack, + resolveChannelAllowlist: resolveSlackChannelAllowlist, + resolveUserAllowlist: resolveSlackUserAllowlist, + sendMessageSlack, + monitorSlackProvider, + handleSlackAction, + }; +} diff --git a/src/plugins/runtime/runtime-telegram.ts b/src/plugins/runtime/runtime-telegram.ts new file mode 100644 index 00000000000..864761480ff --- /dev/null +++ b/src/plugins/runtime/runtime-telegram.ts @@ -0,0 +1,61 @@ +import { + auditTelegramGroupMembership, + collectTelegramUnmentionedGroupIds, +} from "../../../extensions/telegram/src/audit.js"; +import { monitorTelegramProvider } from "../../../extensions/telegram/src/monitor.js"; +import { probeTelegram } from "../../../extensions/telegram/src/probe.js"; +import { + deleteMessageTelegram, + editMessageReplyMarkupTelegram, + editMessageTelegram, + pinMessageTelegram, + renameForumTopicTelegram, + sendMessageTelegram, + sendPollTelegram, + sendTypingTelegram, + unpinMessageTelegram, +} from "../../../extensions/telegram/src/send.js"; +import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js"; +import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js"; +import { createTelegramTypingLease } from "./runtime-telegram-typing.js"; +import type { PluginRuntimeChannel } from "./types-channel.js"; + +export function createRuntimeTelegram(): PluginRuntimeChannel["telegram"] { + return { + auditGroupMembership: auditTelegramGroupMembership, + collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds, + probeTelegram, + resolveTelegramToken, + sendMessageTelegram, + sendPollTelegram, + monitorTelegramProvider, + messageActions: telegramMessageActions, + typing: { + pulse: sendTypingTelegram, + start: async ({ to, accountId, cfg, intervalMs, messageThreadId }) => + await createTelegramTypingLease({ + to, + accountId, + cfg, + intervalMs, + messageThreadId, + pulse: async ({ to, accountId, cfg, messageThreadId }) => + await sendTypingTelegram(to, { + accountId, + cfg, + messageThreadId, + }), + }), + }, + conversationActions: { + editMessage: editMessageTelegram, + editReplyMarkup: editMessageReplyMarkupTelegram, + clearReplyMarkup: async (chatIdInput, messageIdInput, opts = {}) => + await editMessageReplyMarkupTelegram(chatIdInput, messageIdInput, [], opts), + deleteMessage: deleteMessageTelegram, + renameTopic: renameForumTopicTelegram, + pinMessage: pinMessageTelegram, + unpinMessage: unpinMessageTelegram, + }, + }; +}