mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 03:11:10 +00:00
Plugins: reuse compatible registries for runtime providers
This commit is contained in:
@@ -2,12 +2,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
const { loadOpenClawPluginsMock } = vi.hoisted(() => ({
|
||||
const { loadOpenClawPluginsMock, getCompatibleActivePluginRegistryMock } = vi.hoisted(() => ({
|
||||
loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()),
|
||||
getCompatibleActivePluginRegistryMock: vi.fn<
|
||||
(params?: unknown) => ReturnType<typeof createEmptyPluginRegistry> | undefined
|
||||
>(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: loadOpenClawPluginsMock,
|
||||
getCompatibleActivePluginRegistry: getCompatibleActivePluginRegistryMock,
|
||||
}));
|
||||
|
||||
let getImageGenerationProvider: typeof import("./provider-registry.js").getImageGenerationProvider;
|
||||
@@ -17,6 +21,8 @@ describe("image-generation provider registry", () => {
|
||||
afterEach(() => {
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
|
||||
getCompatibleActivePluginRegistryMock.mockReset();
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(undefined);
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
@@ -50,6 +56,7 @@ describe("image-generation provider registry", () => {
|
||||
},
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(registry);
|
||||
|
||||
const provider = getImageGenerationProvider("custom-image");
|
||||
|
||||
@@ -94,6 +101,7 @@ describe("image-generation provider registry", () => {
|
||||
},
|
||||
);
|
||||
setActivePluginRegistry(registry);
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(registry);
|
||||
|
||||
expect(listImageGenerationProviders().map((provider) => provider.id)).toEqual(["safe-image"]);
|
||||
expect(getImageGenerationProvider("__proto__")).toBeUndefined();
|
||||
|
||||
@@ -2,7 +2,6 @@ import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
||||
import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js";
|
||||
import { getActivePluginRegistryKey } from "../plugins/runtime.js";
|
||||
import type { ImageGenerationProviderPlugin } from "../plugins/types.js";
|
||||
|
||||
const BUILTIN_IMAGE_GENERATION_PROVIDERS: readonly ImageGenerationProviderPlugin[] = [];
|
||||
@@ -26,8 +25,6 @@ function resolvePluginImageGenerationProviders(
|
||||
return resolvePluginCapabilityProviders({
|
||||
key: "imageGenerationProviders",
|
||||
cfg,
|
||||
useActiveRegistryWhen: (active) =>
|
||||
(active?.imageGenerationProviders?.length ?? 0) > 0 || Boolean(getActivePluginRegistryKey()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
||||
import { resetPluginRuntimeStateForTest } from "./runtime.js";
|
||||
|
||||
type MockManifestRegistry = {
|
||||
plugins: Array<Record<string, unknown>>;
|
||||
@@ -14,6 +14,9 @@ function createEmptyMockManifestRegistry(): MockManifestRegistry {
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadOpenClawPlugins: vi.fn(() => createEmptyPluginRegistry()),
|
||||
getCompatibleActivePluginRegistry: vi.fn<
|
||||
(params?: unknown) => ReturnType<typeof createEmptyPluginRegistry> | undefined
|
||||
>(() => undefined),
|
||||
loadPluginManifestRegistry: vi.fn<() => MockManifestRegistry>(() =>
|
||||
createEmptyMockManifestRegistry(),
|
||||
),
|
||||
@@ -24,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
|
||||
getCompatibleActivePluginRegistry: mocks.getCompatibleActivePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry.js", () => ({
|
||||
@@ -104,33 +108,14 @@ function setBundledCapabilityFixture(contractKey: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveSpeechCapabilityRegistry(providerId: string) {
|
||||
const active = createEmptyPluginRegistry();
|
||||
active.speechProviders.push({
|
||||
pluginId: providerId,
|
||||
pluginName: "OpenAI",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: providerId,
|
||||
label: "OpenAI",
|
||||
isConfigured: () => true,
|
||||
synthesize: async () => ({
|
||||
audioBuffer: Buffer.from("x"),
|
||||
outputFormat: "mp3",
|
||||
voiceCompatible: false,
|
||||
fileExtension: ".mp3",
|
||||
}),
|
||||
},
|
||||
});
|
||||
setActivePluginRegistry(active);
|
||||
}
|
||||
|
||||
describe("resolvePluginCapabilityProviders", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
resetPluginRuntimeStateForTest();
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.getCompatibleActivePluginRegistry.mockReset();
|
||||
mocks.getCompatibleActivePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.loadPluginManifestRegistry.mockReset();
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry());
|
||||
mocks.withBundledPluginAllowlistCompat.mockReset();
|
||||
@@ -143,7 +128,24 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
});
|
||||
|
||||
it("uses the active registry when capability providers are already loaded", () => {
|
||||
setActiveSpeechCapabilityRegistry("openai");
|
||||
const active = createEmptyPluginRegistry();
|
||||
active.speechProviders.push({
|
||||
pluginId: "openai",
|
||||
pluginName: "OpenAI",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "openai",
|
||||
label: "OpenAI",
|
||||
isConfigured: () => true,
|
||||
synthesize: async () => ({
|
||||
audioBuffer: Buffer.from("x"),
|
||||
outputFormat: "mp3",
|
||||
voiceCompatible: false,
|
||||
fileExtension: ".mp3",
|
||||
}),
|
||||
},
|
||||
});
|
||||
mocks.getCompatibleActivePluginRegistry.mockReturnValue(active);
|
||||
|
||||
const providers = resolvePluginCapabilityProviders({ key: "speechProviders" });
|
||||
|
||||
@@ -171,4 +173,17 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
enablementCompat,
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry even when the capability list is empty", () => {
|
||||
const active = createEmptyPluginRegistry();
|
||||
mocks.getCompatibleActivePluginRegistry.mockReturnValue(active);
|
||||
|
||||
const providers = resolvePluginCapabilityProviders({
|
||||
key: "mediaUnderstandingProviders",
|
||||
cfg: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(providers).toEqual([]);
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
type CapabilityProviderRegistryKey =
|
||||
| "speechProviders"
|
||||
@@ -67,17 +66,17 @@ function resolveCapabilityProviderConfig(params: {
|
||||
export function resolvePluginCapabilityProviders<K extends CapabilityProviderRegistryKey>(params: {
|
||||
key: K;
|
||||
cfg?: OpenClawConfig;
|
||||
useActiveRegistryWhen?: (active: PluginRegistry | undefined) => boolean;
|
||||
}): CapabilityProviderForKey<K>[] {
|
||||
const active = getActivePluginRegistry() ?? undefined;
|
||||
const shouldUseActive =
|
||||
params.useActiveRegistryWhen?.(active) ?? (active?.[params.key].length ?? 0) > 0;
|
||||
const registry =
|
||||
shouldUseActive || !params.cfg
|
||||
? active
|
||||
: loadOpenClawPlugins({
|
||||
const loadOptions =
|
||||
params.cfg === undefined
|
||||
? undefined
|
||||
: {
|
||||
config: resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg }),
|
||||
});
|
||||
};
|
||||
const registry =
|
||||
(loadOptions ? getCompatibleActivePluginRegistry(loadOptions) : undefined) ??
|
||||
(loadOptions ? loadOpenClawPlugins(loadOptions) : undefined) ??
|
||||
getCompatibleActivePluginRegistry();
|
||||
return (registry?.[params.key] ?? []).map(
|
||||
(entry) => entry.provider,
|
||||
) as CapabilityProviderForKey<K>[];
|
||||
|
||||
@@ -8,10 +8,13 @@ type MockRegistryToolEntry = {
|
||||
};
|
||||
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
const getCompatibleActivePluginRegistryMock = vi.fn();
|
||||
const applyPluginAutoEnableMock = vi.fn();
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params),
|
||||
getCompatibleActivePluginRegistry: (params: unknown) =>
|
||||
getCompatibleActivePluginRegistryMock(params),
|
||||
}));
|
||||
|
||||
vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||
@@ -20,7 +23,6 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||
|
||||
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
|
||||
let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;
|
||||
let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
|
||||
|
||||
function makeTool(name: string) {
|
||||
return {
|
||||
@@ -135,13 +137,14 @@ describe("resolvePluginTools optional tools", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
loadOpenClawPluginsMock.mockClear();
|
||||
getCompatibleActivePluginRegistryMock.mockReset();
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(undefined);
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockImplementation(({ config }: { config: unknown }) => ({
|
||||
config,
|
||||
changes: [],
|
||||
}));
|
||||
({ resetPluginRuntimeStateForTest } = await import("./runtime.js"));
|
||||
({ setActivePluginRegistry } = await import("./runtime.js"));
|
||||
resetPluginRuntimeStateForTest();
|
||||
({ resolvePluginTools } = await import("./tools.js"));
|
||||
});
|
||||
@@ -289,31 +292,6 @@ describe("resolvePluginTools optional tools", () => {
|
||||
},
|
||||
};
|
||||
applyPluginAutoEnableMock.mockReturnValue({ config: autoEnabledConfig, changes: [] });
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
gatewayHandlers: {},
|
||||
gatewayMethodScopes: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"stale-registry",
|
||||
);
|
||||
|
||||
const tools = resolvePluginTools({
|
||||
context: {
|
||||
@@ -326,4 +304,28 @@ describe("resolvePluginTools optional tools", () => {
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expectLoaderCall({ config: autoEnabledConfig });
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry instead of loading again", () => {
|
||||
const activeRegistry = {
|
||||
tools: [
|
||||
{
|
||||
pluginId: "optional-demo",
|
||||
optional: true,
|
||||
source: "/tmp/optional-demo.js",
|
||||
factory: () => makeTool("optional_tool"),
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
toolAllowlist: ["optional_tool"],
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,8 @@ import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { getActivePluginRegistry, getActivePluginRegistryKey } from "./runtime.js";
|
||||
import type { OpenClawPluginToolContext } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
@@ -69,21 +68,19 @@ export function resolvePluginTools(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
const loadOptions = {
|
||||
config: effectiveConfig,
|
||||
workspaceDir: params.context.workspaceDir,
|
||||
runtimeOptions: params.allowGatewaySubagentBinding
|
||||
? {
|
||||
allowGatewaySubagentBinding: true,
|
||||
}
|
||||
: undefined,
|
||||
env,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
};
|
||||
const registry =
|
||||
getActivePluginRegistryKey() && activeRegistry && effectiveConfig === baseConfig
|
||||
? activeRegistry
|
||||
: loadOpenClawPlugins({
|
||||
config: effectiveConfig,
|
||||
workspaceDir: params.context.workspaceDir,
|
||||
runtimeOptions: params.allowGatewaySubagentBinding
|
||||
? {
|
||||
allowGatewaySubagentBinding: true,
|
||||
}
|
||||
: undefined,
|
||||
env,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
getCompatibleActivePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions);
|
||||
|
||||
const tools: AnyAgentTool[] = [];
|
||||
const existing = params.existingToolNames ?? new Set<string>();
|
||||
|
||||
@@ -5,10 +5,14 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plug
|
||||
import type { SpeechProviderPlugin } from "../plugins/types.js";
|
||||
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
const getCompatibleActivePluginRegistryMock = vi.fn();
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<typeof loadOpenClawPluginsMock>) =>
|
||||
loadOpenClawPluginsMock(...args),
|
||||
getCompatibleActivePluginRegistry: (
|
||||
...args: Parameters<typeof getCompatibleActivePluginRegistryMock>
|
||||
) => getCompatibleActivePluginRegistryMock(...args),
|
||||
}));
|
||||
|
||||
let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider;
|
||||
@@ -37,6 +41,8 @@ describe("speech provider registry", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
|
||||
getCompatibleActivePluginRegistryMock.mockReset();
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue(undefined);
|
||||
({
|
||||
getSpeechProvider,
|
||||
listSpeechProviders,
|
||||
@@ -60,7 +66,16 @@ describe("speech provider registry", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue({
|
||||
...createEmptyPluginRegistry(),
|
||||
speechProviders: [
|
||||
{
|
||||
pluginId: "test-demo-speech",
|
||||
source: "test",
|
||||
provider: createSpeechProvider("demo-speech"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const providers = listSpeechProviders();
|
||||
|
||||
expect(providers.map((provider) => provider.id)).toEqual(["demo-speech"]);
|
||||
@@ -112,6 +127,16 @@ describe("speech provider registry", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
getCompatibleActivePluginRegistryMock.mockReturnValue({
|
||||
...createEmptyPluginRegistry(),
|
||||
speechProviders: [
|
||||
{
|
||||
pluginId: "test-microsoft",
|
||||
source: "test",
|
||||
provider: createSpeechProvider("microsoft", ["edge"]),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(normalizeSpeechProviderId("edge")).toBe("edge");
|
||||
expect(canonicalizeSpeechProviderId("edge")).toBe("microsoft");
|
||||
|
||||
Reference in New Issue
Block a user