diff --git a/src/channels/dock.ts b/src/channels/dock.ts index c5139c76116..1f19251d535 100644 --- a/src/channels/dock.ts +++ b/src/channels/dock.ts @@ -9,6 +9,7 @@ import { resolveChannelGroupToolsPolicy, } from "../config/group-policy.js"; import { requireActiveExtensionHostRegistry } from "../extension-host/active-registry.js"; +import { listExtensionHostChannelRegistrations } from "../extension-host/runtime-registry.js"; import { formatAllowFromLowercase, formatNormalizedAllowFromEntries, @@ -585,7 +586,7 @@ function listPluginDockEntries(): Array<{ id: ChannelId; dock: ChannelDock; orde const registry = requireActiveExtensionHostRegistry(); const entries: Array<{ id: ChannelId; dock: ChannelDock; order?: number }> = []; const seen = new Set(); - for (const entry of registry.channels) { + for (const entry of listExtensionHostChannelRegistrations(registry)) { const plugin = entry.plugin; const id = String(plugin.id).trim(); if (!id || seen.has(id)) { @@ -628,7 +629,9 @@ export function getChannelDock(id: ChannelId): ChannelDock | undefined { return core; } const registry = requireActiveExtensionHostRegistry(); - const pluginEntry = registry.channels.find((entry) => entry.plugin.id === id); + const pluginEntry = listExtensionHostChannelRegistrations(registry).find( + (entry) => entry.plugin.id === id, + ); if (!pluginEntry) { return undefined; } diff --git a/src/channels/plugins/index.ts b/src/channels/plugins/index.ts index feeb34a4324..8525ef55b1f 100644 --- a/src/channels/plugins/index.ts +++ b/src/channels/plugins/index.ts @@ -2,6 +2,7 @@ import { getActiveExtensionHostRegistryVersion, requireActiveExtensionHostRegistry, } from "../../extension-host/active-registry.js"; +import { listExtensionHostChannelRegistrations } from "../../extension-host/runtime-registry.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; @@ -47,7 +48,9 @@ function resolveCachedChannelPlugins(): CachedChannelPlugins { return cached; } - const sorted = dedupeChannels(registry.channels.map((entry) => entry.plugin)).toSorted((a, b) => { + const sorted = dedupeChannels( + listExtensionHostChannelRegistrations(registry).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); diff --git a/src/channels/plugins/plugins-core.test.ts b/src/channels/plugins/plugins-core.test.ts index 8297a6b7519..dd27de2c743 100644 --- a/src/channels/plugins/plugins-core.test.ts +++ b/src/channels/plugins/plugins-core.test.ts @@ -10,6 +10,7 @@ import type { SlackProbe } from "../../../extensions/slack/src/probe.js"; import type { TelegramProbe } from "../../../extensions/telegram/src/probe.js"; import type { TelegramTokenResolution } from "../../../extensions/telegram/src/token.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { addExtensionHostChannelRegistration } from "../../extension-host/runtime-registry.js"; import type { LineProbeResult } from "../../line/types.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { @@ -96,13 +97,12 @@ describe("channel plugin registry", () => { setActivePluginRegistry(registry, "registry-test"); expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["slack"]); - registry.channels = [ - { - pluginId: "telegram", - plugin: createPlugin("telegram"), - source: "test", - }, - ] as typeof registry.channels; + registry.channels = [] as typeof registry.channels; + addExtensionHostChannelRegistration(registry, { + pluginId: "telegram", + plugin: createPlugin("telegram"), + source: "test", + }); setActivePluginRegistry(registry, "registry-test"); expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["telegram"]); diff --git a/src/channels/plugins/registry-loader.ts b/src/channels/plugins/registry-loader.ts index cb4130787b5..f852cd8da03 100644 --- a/src/channels/plugins/registry-loader.ts +++ b/src/channels/plugins/registry-loader.ts @@ -1,4 +1,5 @@ import { getActiveExtensionHostRegistry } from "../../extension-host/active-registry.js"; +import { listExtensionHostChannelRegistrations } from "../../extension-host/runtime-registry.js"; import type { PluginChannelRegistration, PluginRegistry } from "../../plugins/registry.js"; import type { ChannelId } from "./types.js"; @@ -22,7 +23,9 @@ export function createChannelRegistryLoader( if (cached) { return cached; } - const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id); + const pluginEntry = registry + ? listExtensionHostChannelRegistrations(registry).find((entry) => entry.plugin.id === id) + : undefined; if (!pluginEntry) { return undefined; } diff --git a/src/channels/registry.ts b/src/channels/registry.ts index 052b869f732..4ab1c55030c 100644 --- a/src/channels/registry.ts +++ b/src/channels/registry.ts @@ -1,4 +1,5 @@ import { requireActiveExtensionHostRegistry } from "../extension-host/active-registry.js"; +import { listExtensionHostChannelRegistrations } from "../extension-host/runtime-registry.js"; import type { ChannelMeta } from "./plugins/types.js"; import type { ChannelId } from "./plugins/types.js"; @@ -170,7 +171,7 @@ export function normalizeAnyChannelId(raw?: string | null): ChannelId | null { } const registry = requireActiveExtensionHostRegistry(); - const hit = registry.channels.find((entry) => { + const hit = listExtensionHostChannelRegistrations(registry).find((entry) => { const id = String(entry.plugin.id ?? "") .trim() .toLowerCase(); diff --git a/src/extension-host/plugin-registry-registrations.ts b/src/extension-host/plugin-registry-registrations.ts index d162ff5e077..2f34c2de7c0 100644 --- a/src/extension-host/plugin-registry-registrations.ts +++ b/src/extension-host/plugin-registry-registrations.ts @@ -44,6 +44,7 @@ import { resolveExtensionTypedHookRegistration, } from "./runtime-registrations.js"; import { + listExtensionHostChannelRegistrations, getExtensionHostGatewayHandlers, listExtensionHostHttpRoutes, } from "./runtime-registry.js"; @@ -182,7 +183,7 @@ export function createExtensionHostPluginRegistrationActions(params: { registration: OpenClawPluginChannelRegistration | ChannelPlugin, ) => { const result = resolveExtensionChannelRegistration({ - existing: registry.channels, + existing: [...listExtensionHostChannelRegistrations(registry)], ownerPluginId: record.id, ownerSource: record.source, registration, diff --git a/src/extension-host/registry-writes.ts b/src/extension-host/registry-writes.ts index fbf3a27a0a5..87b67a6806c 100644 --- a/src/extension-host/registry-writes.ts +++ b/src/extension-host/registry-writes.ts @@ -25,6 +25,7 @@ import type { ExtensionHostToolRegistration, } from "./runtime-registrations.js"; import { + addExtensionHostChannelRegistration, addExtensionHostCliRegistration, addExtensionHostHttpRoute, addExtensionHostProviderRegistration, @@ -78,7 +79,7 @@ export function addExtensionChannelRegistration(params: { entry: ExtensionHostChannelRegistration; }): void { params.record.channelIds.push(params.channelId); - params.registry.channels.push(params.entry as PluginChannelRegistration); + addExtensionHostChannelRegistration(params.registry, params.entry as PluginChannelRegistration); } export function addExtensionProviderRegistration(params: { diff --git a/src/extension-host/runtime-registry.test.ts b/src/extension-host/runtime-registry.test.ts index 7f0094dd096..47965b43af3 100644 --- a/src/extension-host/runtime-registry.test.ts +++ b/src/extension-host/runtime-registry.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { + addExtensionHostChannelRegistration, addExtensionHostCliRegistration, addExtensionHostHttpRoute, addExtensionHostProviderRegistration, @@ -8,6 +9,7 @@ import { addExtensionHostToolRegistration, getExtensionHostGatewayHandlers, hasExtensionHostRuntimeEntries, + listExtensionHostChannelRegistrations, listExtensionHostCliRegistrations, listExtensionHostHttpRoutes, listExtensionHostProviderRegistrations, @@ -43,6 +45,28 @@ describe("extension host runtime registry accessors", () => { }); expect(hasExtensionHostRuntimeEntries(routeRegistry)).toBe(true); + const channelRegistry = createEmptyPluginRegistry(); + addExtensionHostChannelRegistration(channelRegistry, { + pluginId: "channel-demo", + source: "test", + plugin: { + id: "channel-demo", + meta: { + id: "channel-demo", + label: "Channel Demo", + selectionLabel: "Channel Demo", + docsPath: "/channels/channel-demo", + blurb: "demo", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({}), + }, + }, + }); + expect(hasExtensionHostRuntimeEntries(channelRegistry)).toBe(true); + const gatewayRegistry = createEmptyPluginRegistry(); setExtensionHostGatewayHandler({ registry: gatewayRegistry, @@ -75,6 +99,7 @@ describe("extension host runtime registry accessors", () => { it("returns stable empty views for missing registries", () => { expect(hasExtensionHostRuntimeEntries(null)).toBe(false); expect(listExtensionHostProviderRegistrations(null)).toEqual([]); + expect(listExtensionHostChannelRegistrations(null)).toEqual([]); expect(listExtensionHostToolRegistrations(null)).toEqual([]); expect(listExtensionHostServiceRegistrations(null)).toEqual([]); expect(listExtensionHostCliRegistrations(null)).toEqual([]); @@ -136,6 +161,27 @@ describe("extension host runtime registry accessors", () => { handler, }); + addExtensionHostChannelRegistration(registry, { + pluginId: "channel-demo", + source: "test", + plugin: { + id: "channel-demo", + meta: { + id: "channel-demo", + label: "Channel Demo", + selectionLabel: "Channel Demo", + docsPath: "/channels/channel-demo", + blurb: "demo", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({}), + }, + }, + }); + + expect(listExtensionHostChannelRegistrations(registry)).toEqual(registry.channels); expect(listExtensionHostToolRegistrations(registry)).toEqual(registry.tools); expect(listExtensionHostProviderRegistrations(registry)).toEqual(registry.providers); expect(listExtensionHostServiceRegistrations(registry)).toEqual(registry.services); @@ -229,4 +275,32 @@ describe("extension host runtime registry accessors", () => { expect(registry.tools[0]?.factory).toBe(factory); expect(registry.providers[0]?.provider).toBe(provider); }); + + it("keeps legacy channel mirrors synchronized with host-owned state", () => { + const registry = createEmptyPluginRegistry(); + const plugin = { + id: "channel-demo", + meta: { + id: "channel-demo", + label: "Channel Demo", + selectionLabel: "Channel Demo", + docsPath: "/channels/channel-demo", + blurb: "demo", + }, + capabilities: { chatTypes: ["direct"] as const }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({}), + }, + }; + + addExtensionHostChannelRegistration(registry, { + pluginId: "channel-demo", + source: "test", + plugin, + }); + + expect(listExtensionHostChannelRegistrations(registry)).toEqual(registry.channels); + expect(registry.channels[0]?.plugin).toBe(plugin); + }); }); diff --git a/src/extension-host/runtime-registry.ts b/src/extension-host/runtime-registry.ts index 15a914fb3ba..64a1ce7e54a 100644 --- a/src/extension-host/runtime-registry.ts +++ b/src/extension-host/runtime-registry.ts @@ -1,5 +1,6 @@ import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js"; import type { + PluginChannelRegistration, PluginCliRegistration, PluginHttpRouteRegistration, PluginProviderRegistration, @@ -10,6 +11,7 @@ import type { const EMPTY_PROVIDERS: readonly PluginProviderRegistration[] = []; const EMPTY_TOOLS: readonly PluginToolRegistration[] = []; +const EMPTY_CHANNELS: readonly PluginChannelRegistration[] = []; const EMPTY_SERVICES: readonly PluginServiceRegistration[] = []; const EMPTY_CLI_REGISTRARS: readonly PluginCliRegistration[] = []; const EMPTY_HTTP_ROUTES: readonly PluginHttpRouteRegistration[] = []; @@ -17,6 +19,8 @@ const EMPTY_GATEWAY_HANDLERS: Readonly = Object.freeze({ const EXTENSION_HOST_RUNTIME_REGISTRY_STATE = Symbol.for("openclaw.extensionHostRuntimeRegistry"); type ExtensionHostRuntimeRegistryState = { + channels: PluginChannelRegistration[]; + legacyChannels: PluginChannelRegistration[]; tools: PluginToolRegistration[]; legacyTools: PluginToolRegistration[]; providers: PluginProviderRegistration[]; @@ -33,7 +37,13 @@ type ExtensionHostRuntimeRegistryState = { type RuntimeRegistryBackedPluginRegistry = Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" > & { [EXTENSION_HOST_RUNTIME_REGISTRY_STATE]?: ExtensionHostRuntimeRegistryState; }; @@ -41,8 +51,37 @@ type RuntimeRegistryBackedPluginRegistry = Pick< function ensureExtensionHostRuntimeRegistryState( registry: RuntimeRegistryBackedPluginRegistry, ): ExtensionHostRuntimeRegistryState { - if (registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE]) { - return registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE]; + const existing = registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE]; + if (existing) { + if (registry.channels !== existing.legacyChannels) { + existing.legacyChannels = registry.channels ?? []; + existing.channels = [...existing.legacyChannels]; + } + if (registry.tools !== existing.legacyTools) { + existing.legacyTools = registry.tools ?? []; + existing.tools = [...existing.legacyTools]; + } + if (registry.providers !== existing.legacyProviders) { + existing.legacyProviders = registry.providers ?? []; + existing.providers = [...existing.legacyProviders]; + } + if (registry.cliRegistrars !== existing.legacyCliRegistrars) { + existing.legacyCliRegistrars = registry.cliRegistrars ?? []; + existing.cliRegistrars = [...existing.legacyCliRegistrars]; + } + if (registry.services !== existing.legacyServices) { + existing.legacyServices = registry.services ?? []; + existing.services = [...existing.legacyServices]; + } + if (registry.httpRoutes !== existing.legacyHttpRoutes) { + existing.legacyHttpRoutes = registry.httpRoutes ?? []; + existing.httpRoutes = [...existing.legacyHttpRoutes]; + } + if (registry.gatewayHandlers !== existing.legacyGatewayHandlers) { + existing.legacyGatewayHandlers = registry.gatewayHandlers ?? {}; + existing.gatewayHandlers = { ...existing.legacyGatewayHandlers }; + } + return existing; } const legacyHttpRoutes = registry.httpRoutes ?? []; @@ -53,12 +92,16 @@ function ensureExtensionHostRuntimeRegistryState( registry.cliRegistrars = legacyCliRegistrars; const legacyServices = registry.services ?? []; registry.services = legacyServices; + const legacyChannels = registry.channels ?? []; + registry.channels = legacyChannels; const legacyTools = registry.tools ?? []; registry.tools = legacyTools; const legacyProviders = registry.providers ?? []; registry.providers = legacyProviders; const state: ExtensionHostRuntimeRegistryState = { + channels: [...legacyChannels], + legacyChannels, tools: [...legacyTools], legacyTools, providers: [...legacyProviders], @@ -76,6 +119,10 @@ function ensureExtensionHostRuntimeRegistryState( return state; } +function syncLegacyChannels(state: ExtensionHostRuntimeRegistryState): void { + state.legacyChannels.splice(0, state.legacyChannels.length, ...state.channels); +} + function syncLegacyTools(state: ExtensionHostRuntimeRegistryState): void { state.legacyTools.splice(0, state.legacyTools.length, ...state.tools); } @@ -129,7 +176,7 @@ export function hasExtensionHostRuntimeEntries( } return ( registry.plugins.length > 0 || - registry.channels.length > 0 || + listExtensionHostChannelRegistrations(registry).length > 0 || listExtensionHostToolRegistrations(registry).length > 0 || listExtensionHostProviderRegistrations(registry).length > 0 || Object.keys(getExtensionHostGatewayHandlers(registry)).length > 0 || @@ -146,7 +193,13 @@ export function listExtensionHostProviderRegistrations( registry: | Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" > | null | undefined, @@ -162,7 +215,13 @@ export function listExtensionHostToolRegistrations( registry: | Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" > | null | undefined, @@ -174,9 +233,40 @@ export function listExtensionHostToolRegistrations( .tools; } +export function listExtensionHostChannelRegistrations( + registry: + | Pick< + PluginRegistry, + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" + > + | null + | undefined, +): readonly PluginChannelRegistration[] { + if (!registry) { + return EMPTY_CHANNELS; + } + return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry) + .channels; +} + export function listExtensionHostServiceRegistrations( registry: - | Pick + | Pick< + PluginRegistry, + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" + > | null | undefined, ): readonly PluginServiceRegistration[] { @@ -189,7 +279,16 @@ export function listExtensionHostServiceRegistrations( export function listExtensionHostCliRegistrations( registry: - | Pick + | Pick< + PluginRegistry, + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" + > | null | undefined, ): readonly PluginCliRegistration[] { @@ -204,7 +303,13 @@ export function listExtensionHostHttpRoutes( registry: | Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" > | null | undefined, @@ -220,7 +325,13 @@ export function getExtensionHostGatewayHandlers( registry: | Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" > | null | undefined, @@ -235,7 +346,13 @@ export function getExtensionHostGatewayHandlers( export function addExtensionHostHttpRoute( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginHttpRouteRegistration, ): void { @@ -249,7 +366,13 @@ export function addExtensionHostHttpRoute( export function replaceExtensionHostHttpRoute(params: { registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >; index: number; entry: PluginHttpRouteRegistration; @@ -264,7 +387,13 @@ export function replaceExtensionHostHttpRoute(params: { export function removeExtensionHostHttpRoute( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginHttpRouteRegistration, ): void { @@ -282,7 +411,13 @@ export function removeExtensionHostHttpRoute( export function setExtensionHostGatewayHandler(params: { registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >; method: string; handler: GatewayRequestHandlers[string]; @@ -297,7 +432,13 @@ export function setExtensionHostGatewayHandler(params: { export function addExtensionHostCliRegistration( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginCliRegistration, ): void { @@ -311,7 +452,13 @@ export function addExtensionHostCliRegistration( export function addExtensionHostServiceRegistration( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginServiceRegistration, ): void { @@ -325,7 +472,13 @@ export function addExtensionHostServiceRegistration( export function addExtensionHostToolRegistration( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginToolRegistration, ): void { @@ -339,7 +492,13 @@ export function addExtensionHostToolRegistration( export function addExtensionHostProviderRegistration( registry: Pick< PluginRegistry, - "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" >, entry: PluginProviderRegistration, ): void { @@ -349,3 +508,23 @@ export function addExtensionHostProviderRegistration( state.providers.push(entry); syncLegacyProviders(state); } + +export function addExtensionHostChannelRegistration( + registry: Pick< + PluginRegistry, + | "channels" + | "tools" + | "providers" + | "cliRegistrars" + | "services" + | "httpRoutes" + | "gatewayHandlers" + >, + entry: PluginChannelRegistration, +): void { + const state = ensureExtensionHostRuntimeRegistryState( + registry as RuntimeRegistryBackedPluginRegistry, + ); + state.channels.push(entry); + syncLegacyChannels(state); +}