refactor: decouple channel setup discovery
This commit is contained in:
parent
963237a18f
commit
74c762beb0
@ -81,6 +81,12 @@ const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry =>
|
||||
typedHooks: [],
|
||||
commands: [],
|
||||
channels,
|
||||
channelSetups: channels.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
plugin: entry.plugin,
|
||||
source: entry.source,
|
||||
enabled: true,
|
||||
})),
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
|
||||
80
src/channels/plugins/setup-registry.ts
Normal file
80
src/channels/plugins/setup-registry.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
getActivePluginRegistryVersion,
|
||||
requireActivePluginRegistry,
|
||||
} from "../../plugins/runtime.js";
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
type CachedChannelSetupPlugins = {
|
||||
registryVersion: number;
|
||||
sorted: ChannelPlugin[];
|
||||
byId: Map<string, ChannelPlugin>;
|
||||
};
|
||||
|
||||
const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = {
|
||||
registryVersion: -1,
|
||||
sorted: [],
|
||||
byId: new Map(),
|
||||
};
|
||||
|
||||
let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE;
|
||||
|
||||
function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] {
|
||||
const seen = new Set<string>();
|
||||
const resolved: ChannelPlugin[] = [];
|
||||
for (const plugin of plugins) {
|
||||
const id = String(plugin.id).trim();
|
||||
if (!id || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
resolved.push(plugin);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins {
|
||||
const registry = requireActivePluginRegistry();
|
||||
const registryVersion = getActivePluginRegistryVersion();
|
||||
const cached = cachedChannelSetupPlugins;
|
||||
if (cached.registryVersion === registryVersion) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const sorted = dedupeSetupPlugins(
|
||||
(registry.channelSetups ?? []).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<string, ChannelPlugin>();
|
||||
for (const plugin of sorted) {
|
||||
byId.set(plugin.id, plugin);
|
||||
}
|
||||
|
||||
const next: CachedChannelSetupPlugins = {
|
||||
registryVersion,
|
||||
sorted,
|
||||
byId,
|
||||
};
|
||||
cachedChannelSetupPlugins = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function listChannelSetupPlugins(): ChannelPlugin[] {
|
||||
return resolveCachedChannelSetupPlugins().sorted.slice();
|
||||
}
|
||||
|
||||
export function getChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
const resolvedId = String(id).trim();
|
||||
if (!resolvedId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveCachedChannelSetupPlugins().byId.get(resolvedId);
|
||||
}
|
||||
@ -307,12 +307,9 @@ describe("setupChannels", () => {
|
||||
|
||||
it("adds disabled hint to channel selection when a channel is disabled", async () => {
|
||||
let selectionCount = 0;
|
||||
const select = vi.fn(async ({ message, options }: { message: string; options: unknown[] }) => {
|
||||
const select = vi.fn(async ({ message }: { message: string; options: unknown[] }) => {
|
||||
if (message === "Select a channel") {
|
||||
selectionCount += 1;
|
||||
const opts = options as Array<{ value: string; hint?: string }>;
|
||||
const telegram = opts.find((opt) => opt.value === "telegram");
|
||||
expect(telegram?.hint).toContain("disabled");
|
||||
return selectionCount === 1 ? "telegram" : "__done__";
|
||||
}
|
||||
if (message.includes("already configured")) {
|
||||
@ -332,6 +329,13 @@ describe("setupChannels", () => {
|
||||
await runSetupChannels(createTelegramCfg("token", false), prompter);
|
||||
|
||||
expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" }));
|
||||
const channelSelectCall = select.mock.calls.find(
|
||||
([params]) => (params as { message?: string }).message === "Select a channel",
|
||||
);
|
||||
const telegramOption = (
|
||||
channelSelectCall?.[0] as { options?: Array<{ value: string; hint?: string }> } | undefined
|
||||
)?.options?.find((opt) => opt.value === "telegram");
|
||||
expect(telegramOption?.hint).toContain("disabled");
|
||||
expect(multiselect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import {
|
||||
getChannelSetupPlugin,
|
||||
listChannelSetupPlugins,
|
||||
} from "../channels/plugins/setup-registry.js";
|
||||
import type { ChannelMeta } from "../channels/plugins/types.js";
|
||||
import {
|
||||
formatChannelPrimerLine,
|
||||
@ -37,7 +40,7 @@ import type {
|
||||
type ConfiguredChannelAction = "update" | "disable" | "delete" | "skip";
|
||||
|
||||
type ChannelStatusSummary = {
|
||||
installedPlugins: ReturnType<typeof listChannelPlugins>;
|
||||
installedPlugins: ReturnType<typeof listChannelSetupPlugins>;
|
||||
catalogEntries: ReturnType<typeof listChannelPluginCatalogEntries>;
|
||||
statusByChannel: Map<ChannelChoice, ChannelOnboardingStatus>;
|
||||
statusLines: string[];
|
||||
@ -90,7 +93,7 @@ async function promptRemovalAccountId(params: {
|
||||
channel: ChannelChoice;
|
||||
}): Promise<string> {
|
||||
const { cfg, prompter, label, channel } = params;
|
||||
const plugin = getChannelPlugin(channel);
|
||||
const plugin = getChannelSetupPlugin(channel);
|
||||
if (!plugin) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
@ -115,7 +118,7 @@ async function collectChannelStatus(params: {
|
||||
options?: SetupChannelsOptions;
|
||||
accountOverrides: Partial<Record<ChannelChoice, string>>;
|
||||
}): Promise<ChannelStatusSummary> {
|
||||
const installedPlugins = listChannelPlugins();
|
||||
const installedPlugins = listChannelSetupPlugins();
|
||||
const installedIds = new Set(installedPlugins.map((plugin) => plugin.id));
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
|
||||
const catalogEntries = listChannelPluginCatalogEntries({ workspaceDir }).filter(
|
||||
@ -297,6 +300,13 @@ export async function setupChannels(
|
||||
options?: SetupChannelsOptions,
|
||||
): Promise<OpenClawConfig> {
|
||||
let next = cfg;
|
||||
if (listChannelOnboardingAdapters().length === 0) {
|
||||
reloadOnboardingPluginRegistry({
|
||||
cfg: next,
|
||||
runtime,
|
||||
workspaceDir: resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)),
|
||||
});
|
||||
}
|
||||
const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []);
|
||||
const accountOverrides: Partial<Record<ChannelChoice, string>> = {
|
||||
...options?.accountIds,
|
||||
@ -366,7 +376,15 @@ export async function setupChannels(
|
||||
};
|
||||
|
||||
const resolveDisabledHint = (channel: ChannelChoice): string | undefined => {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
const plugin = getChannelSetupPlugin(channel);
|
||||
if (
|
||||
typeof (next.channels as Record<string, { enabled?: boolean }> | undefined)?.[channel]
|
||||
?.enabled === "boolean"
|
||||
) {
|
||||
return (next.channels as Record<string, { enabled?: boolean }>)[channel]?.enabled === false
|
||||
? "disabled"
|
||||
: undefined;
|
||||
}
|
||||
if (!plugin) {
|
||||
if (next.plugins?.entries?.[channel]?.enabled === false) {
|
||||
return "plugin disabled";
|
||||
@ -383,11 +401,6 @@ export async function setupChannels(
|
||||
enabled = plugin.config.isEnabled(account, next);
|
||||
} else if (typeof (account as { enabled?: boolean })?.enabled === "boolean") {
|
||||
enabled = (account as { enabled?: boolean }).enabled;
|
||||
} else if (
|
||||
typeof (next.channels as Record<string, { enabled?: boolean }> | undefined)?.[channel]
|
||||
?.enabled === "boolean"
|
||||
) {
|
||||
enabled = (next.channels as Record<string, { enabled?: boolean }>)[channel]?.enabled;
|
||||
}
|
||||
return enabled === false ? "disabled" : undefined;
|
||||
};
|
||||
@ -411,7 +424,7 @@ export async function setupChannels(
|
||||
|
||||
const getChannelEntries = () => {
|
||||
const core = listChatChannels();
|
||||
const installed = listChannelPlugins();
|
||||
const installed = listChannelSetupPlugins();
|
||||
const installedIds = new Set(installed.map((plugin) => plugin.id));
|
||||
const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
||||
const catalog = listChannelPluginCatalogEntries({ workspaceDir }).filter(
|
||||
@ -449,10 +462,7 @@ export async function setupChannels(
|
||||
statusByChannel.set(channel, status);
|
||||
};
|
||||
|
||||
const ensureBundledPluginEnabled = async (channel: ChannelChoice): Promise<boolean> => {
|
||||
if (getChannelPlugin(channel)) {
|
||||
return true;
|
||||
}
|
||||
const enableBundledPluginForSetup = async (channel: ChannelChoice): Promise<boolean> => {
|
||||
const result = enablePluginInConfig(next, channel);
|
||||
next = result.config;
|
||||
if (!result.enabled) {
|
||||
@ -468,24 +478,6 @@ export async function setupChannels(
|
||||
runtime,
|
||||
workspaceDir,
|
||||
});
|
||||
if (!getChannelPlugin(channel)) {
|
||||
// Some installs/environments can fail to populate the plugin registry during onboarding,
|
||||
// even for built-in channels. If the channel supports onboarding, proceed with config
|
||||
// so setup isn't blocked; the gateway can still load plugins on startup.
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
if (adapter) {
|
||||
await prompter.note(
|
||||
`${channel} plugin not available (continuing with onboarding). If the channel still doesn't work after setup, run \`${formatCliCommand(
|
||||
"openclaw plugins list",
|
||||
)}\` and \`${formatCliCommand("openclaw plugins enable " + channel)}\`, then restart the gateway.`,
|
||||
"Channel setup",
|
||||
);
|
||||
await refreshStatus(channel);
|
||||
return true;
|
||||
}
|
||||
await prompter.note(`${channel} plugin not available.`, "Channel setup");
|
||||
return false;
|
||||
}
|
||||
await refreshStatus(channel);
|
||||
return true;
|
||||
};
|
||||
@ -529,7 +521,7 @@ export async function setupChannels(
|
||||
};
|
||||
|
||||
const handleConfiguredChannel = async (channel: ChannelChoice, label: string) => {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
const plugin = getChannelSetupPlugin(channel);
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
if (adapter?.configureWhenConfigured) {
|
||||
const custom = await adapter.configureWhenConfigured({
|
||||
@ -642,13 +634,13 @@ export async function setupChannels(
|
||||
});
|
||||
await refreshStatus(channel);
|
||||
} else {
|
||||
const enabled = await ensureBundledPluginEnabled(channel);
|
||||
const enabled = await enableBundledPluginForSetup(channel);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = getChannelPlugin(channel);
|
||||
const plugin = getChannelSetupPlugin(channel);
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
const label = plugin?.meta.label ?? catalogEntry?.meta.label ?? channel;
|
||||
const status = statusByChannel.get(channel);
|
||||
|
||||
@ -1,34 +1,26 @@
|
||||
import { discordOnboardingAdapter } from "../../../extensions/discord/src/onboarding.js";
|
||||
import { imessageOnboardingAdapter } from "../../../extensions/imessage/src/onboarding.js";
|
||||
import { signalOnboardingAdapter } from "../../../extensions/signal/src/onboarding.js";
|
||||
import { slackOnboardingAdapter } from "../../../extensions/slack/src/onboarding.js";
|
||||
import { telegramOnboardingAdapter } from "../../../extensions/telegram/src/onboarding.js";
|
||||
import { whatsappOnboardingAdapter } from "../../../extensions/whatsapp/src/onboarding.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { listChannelSetupPlugins } from "../../channels/plugins/setup-registry.js";
|
||||
import type { ChannelChoice } from "../onboard-types.js";
|
||||
import type { ChannelOnboardingAdapter } from "./types.js";
|
||||
|
||||
const BUILTIN_ONBOARDING_ADAPTERS: ChannelOnboardingAdapter[] = [
|
||||
telegramOnboardingAdapter,
|
||||
whatsappOnboardingAdapter,
|
||||
discordOnboardingAdapter,
|
||||
slackOnboardingAdapter,
|
||||
signalOnboardingAdapter,
|
||||
imessageOnboardingAdapter,
|
||||
];
|
||||
function resolveChannelOnboardingAdapter(
|
||||
plugin: (typeof listChannelSetupPlugins)[number],
|
||||
): ChannelOnboardingAdapter | undefined {
|
||||
if (plugin.onboarding) {
|
||||
return plugin.onboarding;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const CHANNEL_ONBOARDING_ADAPTERS = () => {
|
||||
const fromRegistry = listChannelPlugins()
|
||||
.map((plugin) => (plugin.onboarding ? ([plugin.id, plugin.onboarding] as const) : null))
|
||||
.filter((entry): entry is readonly [ChannelChoice, ChannelOnboardingAdapter] => Boolean(entry));
|
||||
|
||||
// Fall back to built-in adapters to keep onboarding working even when the plugin registry
|
||||
// fails to populate (see #25545).
|
||||
const fromBuiltins = BUILTIN_ONBOARDING_ADAPTERS.map(
|
||||
(adapter) => [adapter.channel, adapter] as const,
|
||||
);
|
||||
|
||||
return new Map<ChannelChoice, ChannelOnboardingAdapter>([...fromBuiltins, ...fromRegistry]);
|
||||
const adapters = new Map<ChannelChoice, ChannelOnboardingAdapter>();
|
||||
for (const plugin of listChannelSetupPlugins()) {
|
||||
const adapter = resolveChannelOnboardingAdapter(plugin);
|
||||
if (!adapter) {
|
||||
continue;
|
||||
}
|
||||
adapters.set(plugin.id, adapter);
|
||||
}
|
||||
return adapters;
|
||||
};
|
||||
|
||||
export function getChannelOnboardingAdapter(
|
||||
|
||||
@ -26,6 +26,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
commands: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
|
||||
@ -9,6 +9,7 @@ export const registryState: { registry: PluginRegistry } = {
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
|
||||
@ -144,6 +144,7 @@ const createStubPluginRegistry = (): PluginRegistry => ({
|
||||
plugin: createStubChannelPlugin({ id: "bluebubbles", label: "BlueBubbles" }),
|
||||
},
|
||||
],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
|
||||
@ -847,13 +847,23 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
});
|
||||
};
|
||||
|
||||
if (!enableState.enabled) {
|
||||
const registrationMode = enableState.enabled
|
||||
? "full"
|
||||
: !validateOnly && manifestRecord.channels.length > 0
|
||||
? "setup-only"
|
||||
: null;
|
||||
|
||||
if (!registrationMode) {
|
||||
record.status = "disabled";
|
||||
record.error = enableState.reason;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
if (!enableState.enabled) {
|
||||
record.status = "disabled";
|
||||
record.error = enableState.reason;
|
||||
}
|
||||
|
||||
if (record.format === "bundle") {
|
||||
const unsupportedCapabilities = (record.bundleCapabilities ?? []).filter(
|
||||
@ -878,10 +888,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fast-path bundled memory plugins that are guaranteed disabled by slot policy.
|
||||
// This avoids opening/importing heavy memory plugin modules that will never register.
|
||||
if (candidate.origin === "bundled" && manifestRecord.kind === "memory") {
|
||||
if (
|
||||
registrationMode === "full" &&
|
||||
candidate.origin === "bundled" &&
|
||||
manifestRecord.kind === "memory"
|
||||
) {
|
||||
const earlyMemoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: "memory",
|
||||
@ -966,24 +979,26 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
memorySlotMatched = true;
|
||||
}
|
||||
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
if (registrationMode === "full") {
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
|
||||
if (!memoryDecision.enabled) {
|
||||
record.enabled = false;
|
||||
record.status = "disabled";
|
||||
record.error = memoryDecision.reason;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
if (!memoryDecision.enabled) {
|
||||
record.enabled = false;
|
||||
record.status = "disabled";
|
||||
record.error = memoryDecision.reason;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (memoryDecision.selected && record.kind === "memory") {
|
||||
selectedMemoryPluginId = record.id;
|
||||
if (memoryDecision.selected && record.kind === "memory") {
|
||||
selectedMemoryPluginId = record.id;
|
||||
}
|
||||
}
|
||||
|
||||
const validatedConfig = validatePluginConfig({
|
||||
@ -1014,6 +1029,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
config: cfg,
|
||||
pluginConfig: validatedConfig.value,
|
||||
hookPolicy: entry?.hooks,
|
||||
registrationMode,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@ -85,6 +85,15 @@ export type PluginChannelRegistration = {
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginChannelSetupRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
plugin: ChannelPlugin;
|
||||
source: string;
|
||||
enabled: boolean;
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginProviderRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
@ -154,6 +163,7 @@ export type PluginRegistry = {
|
||||
hooks: PluginHookRegistration[];
|
||||
typedHooks: TypedPluginHookRegistration[];
|
||||
channels: PluginChannelRegistration[];
|
||||
channelSetups: PluginChannelSetupRegistration[];
|
||||
providers: PluginProviderRegistration[];
|
||||
gatewayHandlers: GatewayRequestHandlers;
|
||||
httpRoutes: PluginHttpRouteRegistration[];
|
||||
@ -173,6 +183,8 @@ type PluginTypedHookPolicy = {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
|
||||
type PluginRegistrationMode = "full" | "setup-only";
|
||||
|
||||
const constrainLegacyPromptInjectionHook = (
|
||||
handler: PluginHookHandlerMap["before_agent_start"],
|
||||
): PluginHookHandlerMap["before_agent_start"] => {
|
||||
@ -194,6 +206,7 @@ export function createEmptyPluginRegistry(): PluginRegistry {
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
@ -436,6 +449,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
const registerChannel = (
|
||||
record: PluginRecord,
|
||||
registration: OpenClawPluginChannelRegistration | ChannelPlugin,
|
||||
mode: PluginRegistrationMode = "full",
|
||||
) => {
|
||||
const normalized =
|
||||
typeof (registration as OpenClawPluginChannelRegistration).plugin === "object"
|
||||
@ -452,17 +466,38 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existing = registry.channels.find((entry) => entry.plugin.id === id);
|
||||
if (existing) {
|
||||
const existingRuntime = registry.channels.find((entry) => entry.plugin.id === id);
|
||||
if (mode === "full" && existingRuntime) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `channel already registered: ${id} (${existing.pluginId})`,
|
||||
message: `channel already registered: ${id} (${existingRuntime.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existingSetup = registry.channelSetups.find((entry) => entry.plugin.id === id);
|
||||
if (existingSetup) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `channel setup already registered: ${id} (${existingSetup.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
record.channelIds.push(id);
|
||||
registry.channelSetups.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
plugin,
|
||||
source: record.source,
|
||||
enabled: record.enabled,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
if (mode === "setup-only") {
|
||||
return;
|
||||
}
|
||||
registry.channels.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
@ -667,8 +702,10 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
config: OpenClawPluginApi["config"];
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
hookPolicy?: PluginTypedHookPolicy;
|
||||
registrationMode?: PluginRegistrationMode;
|
||||
},
|
||||
): OpenClawPluginApi => {
|
||||
const registrationMode = params.registrationMode ?? "full";
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
@ -680,31 +717,50 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
pluginConfig: params.pluginConfig,
|
||||
runtime: registryParams.runtime,
|
||||
logger: normalizeLogger(registryParams.logger),
|
||||
registerTool: (tool, opts) => registerTool(record, tool, opts),
|
||||
registerHook: (events, handler, opts) =>
|
||||
registerHook(record, events, handler, opts, params.config),
|
||||
registerHttpRoute: (params) => registerHttpRoute(record, params),
|
||||
registerChannel: (registration) => registerChannel(record, registration),
|
||||
registerProvider: (provider) => registerProvider(record, provider),
|
||||
registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler),
|
||||
registerCli: (registrar, opts) => registerCli(record, registrar, opts),
|
||||
registerService: (service) => registerService(record, service),
|
||||
registerInteractiveHandler: (registration) => {
|
||||
const result = registerPluginInteractiveHandler(record.id, registration, {
|
||||
pluginName: record.name,
|
||||
pluginRoot: record.rootDir,
|
||||
});
|
||||
if (!result.ok) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: result.error ?? "interactive handler registration failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
registerCommand: (command) => registerCommand(record, command),
|
||||
registerTool:
|
||||
registrationMode === "full" ? (tool, opts) => registerTool(record, tool, opts) : () => {},
|
||||
registerHook:
|
||||
registrationMode === "full"
|
||||
? (events, handler, opts) => registerHook(record, events, handler, opts, params.config)
|
||||
: () => {},
|
||||
registerHttpRoute:
|
||||
registrationMode === "full" ? (params) => registerHttpRoute(record, params) : () => {},
|
||||
registerChannel: (registration) => registerChannel(record, registration, registrationMode),
|
||||
registerProvider:
|
||||
registrationMode === "full" ? (provider) => registerProvider(record, provider) : () => {},
|
||||
registerGatewayMethod:
|
||||
registrationMode === "full"
|
||||
? (method, handler) => registerGatewayMethod(record, method, handler)
|
||||
: () => {},
|
||||
registerCli:
|
||||
registrationMode === "full"
|
||||
? (registrar, opts) => registerCli(record, registrar, opts)
|
||||
: () => {},
|
||||
registerService:
|
||||
registrationMode === "full" ? (service) => registerService(record, service) : () => {},
|
||||
registerInteractiveHandler:
|
||||
registrationMode === "full"
|
||||
? (registration) => {
|
||||
const result = registerPluginInteractiveHandler(record.id, registration, {
|
||||
pluginName: record.name,
|
||||
pluginRoot: record.rootDir,
|
||||
});
|
||||
if (!result.ok) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: result.error ?? "interactive handler registration failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
: () => {},
|
||||
registerCommand:
|
||||
registrationMode === "full" ? (command) => registerCommand(record, command) : () => {},
|
||||
registerContextEngine: (id, factory) => {
|
||||
if (registrationMode !== "full") {
|
||||
return;
|
||||
}
|
||||
if (id === defaultSlotIdForKey("contextEngine")) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
@ -728,7 +784,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
},
|
||||
resolvePath: (input: string) => resolveUserPath(input),
|
||||
on: (hookName, handler, opts) =>
|
||||
registerTypedHook(record, hookName, handler, opts, params.hookPolicy),
|
||||
registrationMode === "full"
|
||||
? registerTypedHook(record, hookName, handler, opts, params.hookPolicy)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -18,6 +18,12 @@ export const createTestRegistry = (channels: TestChannelRegistration[] = []): Pl
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: channels as unknown as PluginRegistry["channels"],
|
||||
channelSetups: channels.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
plugin: entry.plugin as PluginRegistry["channelSetups"][number]["plugin"],
|
||||
source: entry.source,
|
||||
enabled: true,
|
||||
})),
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user