mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 21:31:26 +00:00
perf(plugins): keep gateway startup channel-only (#59754)
* perf(plugins): keep gateway startup channel-only * fix(gateway): preserve startup sidecars in plugin scope
This commit is contained in:
@@ -277,6 +277,22 @@ describe("loadGatewayPlugins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("treats an empty startup scope as no plugin load instead of an unscoped load", async () => {
|
||||
resolveGatewayStartupPluginIds.mockReturnValue([]);
|
||||
|
||||
const result = serverPluginsModule.loadGatewayPlugins({
|
||||
cfg: {},
|
||||
workspaceDir: "/tmp",
|
||||
log: createTestLog(),
|
||||
coreGatewayHandlers: {},
|
||||
baseMethods: ["sessions.get"],
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
expect(result.pluginRegistry.plugins).toEqual([]);
|
||||
expect(result.gatewayMethods).toEqual(["sessions.get"]);
|
||||
});
|
||||
|
||||
test("loads gateway plugins from the auto-enabled config snapshot", async () => {
|
||||
const autoEnabledConfig = { channels: { slack: { enabled: true } }, autoEnabled: true };
|
||||
applyPluginAutoEnable.mockReturnValue({
|
||||
|
||||
@@ -5,6 +5,8 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { resolveGatewayStartupPluginIds } from "../plugins/channel-plugin-ids.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
@@ -424,6 +426,14 @@ export function loadGatewayPlugins(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
if (pluginIds.length === 0) {
|
||||
const pluginRegistry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(pluginRegistry, undefined, "gateway-bindable");
|
||||
return {
|
||||
pluginRegistry,
|
||||
gatewayMethods: [...params.baseMethods],
|
||||
};
|
||||
}
|
||||
const pluginRegistry = loadOpenClawPlugins({
|
||||
config: resolvedConfig,
|
||||
activationSourceConfig: params.activationSourceConfig ?? params.cfg,
|
||||
|
||||
@@ -26,7 +26,15 @@ function createManifestRegistryFixture() {
|
||||
cliBackends: [],
|
||||
},
|
||||
{
|
||||
id: "demo-default-on-sidecar",
|
||||
id: "demo-other-channel",
|
||||
channels: ["demo-other-channel"],
|
||||
origin: "bundled",
|
||||
enabledByDefault: undefined,
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
{
|
||||
id: "browser",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
@@ -42,7 +50,7 @@ function createManifestRegistryFixture() {
|
||||
cliBackends: ["demo-cli"],
|
||||
},
|
||||
{
|
||||
id: "demo-bundled-sidecar",
|
||||
id: "voice-call",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
enabledByDefault: undefined,
|
||||
@@ -84,17 +92,38 @@ function createStartupConfig(params: {
|
||||
enabledPluginIds?: string[];
|
||||
providerIds?: string[];
|
||||
modelId?: string;
|
||||
channelIds?: string[];
|
||||
allowPluginIds?: string[];
|
||||
noConfiguredChannels?: boolean;
|
||||
}) {
|
||||
return {
|
||||
...(params.noConfiguredChannels
|
||||
? {
|
||||
channels: {},
|
||||
}
|
||||
: params.channelIds?.length
|
||||
? {
|
||||
channels: Object.fromEntries(
|
||||
params.channelIds.map((channelId) => [channelId, { enabled: true }]),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(params.enabledPluginIds?.length
|
||||
? {
|
||||
plugins: {
|
||||
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
|
||||
entries: Object.fromEntries(
|
||||
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
: params.allowPluginIds?.length
|
||||
? {
|
||||
plugins: {
|
||||
allow: params.allowPluginIds,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(params.providerIds?.length
|
||||
? {
|
||||
models: {
|
||||
@@ -127,37 +156,57 @@ function createStartupConfig(params: {
|
||||
|
||||
describe("resolveGatewayStartupPluginIds", () => {
|
||||
beforeEach(() => {
|
||||
listPotentialConfiguredChannelIds.mockReset().mockReturnValue(["demo-channel"]);
|
||||
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
|
||||
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
|
||||
return Object.keys(config.channels ?? {});
|
||||
}
|
||||
return ["demo-channel"];
|
||||
});
|
||||
loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture());
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"includes configured channels and explicitly enabled bundled sidecars",
|
||||
"includes only configured channel plugins at idle startup",
|
||||
createStartupConfig({
|
||||
enabledPluginIds: ["demo-bundled-sidecar"],
|
||||
enabledPluginIds: ["voice-call"],
|
||||
modelId: "demo-cli/demo-model",
|
||||
}),
|
||||
["demo-channel", "demo-provider-plugin", "demo-bundled-sidecar"],
|
||||
["demo-channel", "browser", "voice-call"],
|
||||
],
|
||||
[
|
||||
"skips bundled plugins with enabledByDefault: true until something references them",
|
||||
"keeps bundled startup sidecars with enabledByDefault at idle startup",
|
||||
{} as OpenClawConfig,
|
||||
["demo-channel"],
|
||||
["demo-channel", "browser"],
|
||||
],
|
||||
[
|
||||
"auto-loads bundled plugins referenced by configured provider ids",
|
||||
"keeps provider plugins out of idle startup when only provider config references them",
|
||||
createStartupConfig({
|
||||
providerIds: ["demo-provider"],
|
||||
}),
|
||||
["demo-channel", "demo-provider-plugin"],
|
||||
["demo-channel", "browser"],
|
||||
],
|
||||
[
|
||||
"keeps non-bundled sidecars out of startup unless explicitly enabled",
|
||||
"includes explicitly enabled non-channel sidecars in startup scope",
|
||||
createStartupConfig({
|
||||
enabledPluginIds: ["demo-global-sidecar"],
|
||||
enabledPluginIds: ["demo-global-sidecar", "voice-call"],
|
||||
}),
|
||||
["demo-channel", "demo-global-sidecar"],
|
||||
["demo-channel", "browser", "voice-call", "demo-global-sidecar"],
|
||||
],
|
||||
[
|
||||
"keeps default-enabled startup sidecars when a restrictive allowlist permits them",
|
||||
createStartupConfig({
|
||||
allowPluginIds: ["browser"],
|
||||
noConfiguredChannels: true,
|
||||
}),
|
||||
["browser"],
|
||||
],
|
||||
[
|
||||
"includes every configured channel plugin and excludes other channels",
|
||||
createStartupConfig({
|
||||
channelIds: ["demo-channel", "demo-other-channel"],
|
||||
}),
|
||||
["demo-channel", "demo-other-channel", "browser"],
|
||||
],
|
||||
] as const)("%s", (_name, config, expected) => {
|
||||
expectStartupPluginIdsCase({ config, expected });
|
||||
|
||||
@@ -1,239 +1,24 @@
|
||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
normalizeProviderId,
|
||||
resolveModelRefFromString,
|
||||
} from "../agents/model-selection.js";
|
||||
import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolvePluginWebSearchConfig } from "../config/legacy-web-search.js";
|
||||
import {
|
||||
resolveAgentModelFallbackValues,
|
||||
resolveAgentModelPrimaryValue,
|
||||
} from "../config/model-input.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { hasKind } from "./slots.js";
|
||||
|
||||
type ModelListLike = string | { primary?: string; fallbacks?: string[] } | undefined;
|
||||
|
||||
function addResolvedActivationId(params: {
|
||||
raw: string | undefined;
|
||||
activationIds: Set<string>;
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
}): void {
|
||||
const raw = params.raw?.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
params.activationIds.add(normalizeProviderId(resolved.ref.provider));
|
||||
function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
|
||||
return Boolean(
|
||||
plugin.providers.length > 0 ||
|
||||
plugin.cliBackends.length > 0 ||
|
||||
plugin.contracts?.speechProviders?.length ||
|
||||
plugin.contracts?.mediaUnderstandingProviders?.length ||
|
||||
plugin.contracts?.imageGenerationProviders?.length ||
|
||||
plugin.contracts?.webFetchProviders?.length ||
|
||||
plugin.contracts?.webSearchProviders?.length ||
|
||||
hasKind(plugin.kind, "memory"),
|
||||
);
|
||||
}
|
||||
|
||||
function addModelListActivationIds(params: {
|
||||
value: ModelListLike;
|
||||
activationIds: Set<string>;
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
}): void {
|
||||
addResolvedActivationId({
|
||||
raw: resolveAgentModelPrimaryValue(params.value),
|
||||
activationIds: params.activationIds,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
for (const fallback of resolveAgentModelFallbackValues(params.value)) {
|
||||
addResolvedActivationId({
|
||||
raw: fallback,
|
||||
activationIds: params.activationIds,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addProviderModelPairActivationId(params: {
|
||||
provider: string | undefined;
|
||||
model: string | undefined;
|
||||
activationIds: Set<string>;
|
||||
}): void {
|
||||
const provider = normalizeProviderId(params.provider ?? "");
|
||||
const model = params.model?.trim();
|
||||
if (!provider || !model) {
|
||||
return;
|
||||
}
|
||||
params.activationIds.add(provider);
|
||||
}
|
||||
|
||||
function collectConfiguredActivationIds(config: OpenClawConfig): Set<string> {
|
||||
const activationIds = new Set<string>();
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: config,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
|
||||
addModelListActivationIds({ value: config.agents?.defaults?.model, activationIds, aliasIndex });
|
||||
addModelListActivationIds({
|
||||
value: config.agents?.defaults?.imageModel,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addModelListActivationIds({
|
||||
value: config.agents?.defaults?.imageGenerationModel,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addModelListActivationIds({
|
||||
value: config.agents?.defaults?.pdfModel,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: config.agents?.defaults?.compaction?.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: config.agents?.defaults?.heartbeat?.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addModelListActivationIds({
|
||||
value: config.agents?.defaults?.subagents?.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: config.messages?.tts?.summaryModel,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: config.hooks?.gmail?.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
for (const modelRef of Object.keys(config.agents?.defaults?.models ?? {})) {
|
||||
addResolvedActivationId({
|
||||
raw: modelRef,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
}
|
||||
|
||||
for (const providerId of Object.keys(config.agents?.defaults?.cliBackends ?? {})) {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (normalized) {
|
||||
activationIds.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
for (const providerId of Object.keys(config.models?.providers ?? {})) {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (normalized) {
|
||||
activationIds.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
for (const agent of config.agents?.list ?? []) {
|
||||
addModelListActivationIds({ value: agent.model, activationIds, aliasIndex });
|
||||
addModelListActivationIds({ value: agent.subagents?.model, activationIds, aliasIndex });
|
||||
addResolvedActivationId({
|
||||
raw: agent.heartbeat?.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
}
|
||||
|
||||
for (const mapping of config.hooks?.mappings ?? []) {
|
||||
addResolvedActivationId({
|
||||
raw: mapping.model,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
}
|
||||
|
||||
for (const channelMap of Object.values(config.channels?.modelByChannel ?? {})) {
|
||||
if (!channelMap || typeof channelMap !== "object") {
|
||||
continue;
|
||||
}
|
||||
for (const raw of Object.values(channelMap)) {
|
||||
addResolvedActivationId({
|
||||
raw: typeof raw === "string" ? raw : undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addResolvedActivationId({
|
||||
raw: config.tools?.subagents?.model
|
||||
? resolveAgentModelPrimaryValue(config.tools?.subagents?.model)
|
||||
: undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
if (config.tools?.subagents?.model) {
|
||||
for (const fallback of resolveAgentModelFallbackValues(config.tools.subagents.model)) {
|
||||
addResolvedActivationId({ raw: fallback, activationIds, aliasIndex });
|
||||
}
|
||||
}
|
||||
|
||||
addResolvedActivationId({
|
||||
raw: resolvePluginWebSearchConfig(config, "google")?.model as string | undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: resolvePluginWebSearchConfig(config, "xai")?.model as string | undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: resolvePluginWebSearchConfig(config, "moonshot")?.model as string | undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
addResolvedActivationId({
|
||||
raw: resolvePluginWebSearchConfig(config, "perplexity")?.model as string | undefined,
|
||||
activationIds,
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
for (const entry of config.tools?.media?.models ?? []) {
|
||||
addProviderModelPairActivationId({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
activationIds,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.image?.models ?? []) {
|
||||
addProviderModelPairActivationId({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
activationIds,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.audio?.models ?? []) {
|
||||
addProviderModelPairActivationId({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
activationIds,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.video?.models ?? []) {
|
||||
addProviderModelPairActivationId({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
activationIds,
|
||||
});
|
||||
}
|
||||
|
||||
return activationIds;
|
||||
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
|
||||
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
|
||||
}
|
||||
|
||||
export function resolveChannelPluginIds(params: {
|
||||
@@ -297,46 +82,32 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
|
||||
);
|
||||
const pluginsConfig = normalizePluginsConfig(params.config.plugins);
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const configuredActivationIds = collectConfiguredActivationIds(params.config);
|
||||
return manifestRegistry.plugins
|
||||
.filter((plugin) => {
|
||||
})
|
||||
.plugins.filter((plugin) => {
|
||||
if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) {
|
||||
return true;
|
||||
}
|
||||
if (plugin.channels.length > 0) {
|
||||
if (!isGatewayStartupSidecar(plugin)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
plugin.origin === "bundled" &&
|
||||
(plugin.providers.some((providerId) =>
|
||||
configuredActivationIds.has(normalizeProviderId(providerId)),
|
||||
) ||
|
||||
plugin.cliBackends.some((backendId) =>
|
||||
configuredActivationIds.has(normalizeProviderId(backendId)),
|
||||
))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const enabled = resolveEffectiveEnableState({
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: plugin.enabledByDefault,
|
||||
}).enabled;
|
||||
if (!enabled) {
|
||||
});
|
||||
if (!activationState.enabled) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
pluginsConfig.allow.includes(plugin.id) ||
|
||||
pluginsConfig.entries[plugin.id]?.enabled === true ||
|
||||
pluginsConfig.slots.memory === plugin.id
|
||||
);
|
||||
if (plugin.origin !== "bundled") {
|
||||
return activationState.explicitlyEnabled;
|
||||
}
|
||||
return activationState.source === "explicit" || activationState.source === "default";
|
||||
})
|
||||
.map((plugin) => plugin.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user