Files
openclaw/src/plugins/plugin-registry-contributions.ts
T
2026-07-31 16:47:45 -07:00

336 lines
11 KiB
TypeScript

/** Loads manifest and installed-index contributions used to build plugin registry snapshots. */
import { normalizeSortedUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
normalizePluginsConfigWithResolver,
type NormalizedPluginsConfig,
} from "./config-normalization-shared.js";
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
import type {
BundledChannelConfigCollector,
PluginManifestContractListKey,
PluginManifestRecord,
PluginManifestRegistry,
} from "./manifest-registry.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
import type { PluginOrigin } from "./plugin-origin.types.js";
import {
createPluginRegistryIdNormalizer,
type PluginRegistryIdNormalizerOptions,
} from "./plugin-registry-id-normalizer.js";
import {
loadPluginRegistrySnapshot,
loadPluginRegistrySnapshotWithMetadata,
type LoadPluginRegistryParams,
type PluginRegistrySnapshot,
} from "./plugin-registry-snapshot.js";
export { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalizer.js";
type PluginLookUpTable = Pick<
PluginMetadataSnapshot,
"index" | "manifestRegistry" | "plugins" | "normalizePluginId" | "owners"
>;
type PluginRegistryContributionOptions = LoadPluginRegistryParams & {
includeDisabled?: boolean;
lookUpTable?: PluginLookUpTable;
};
type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & {
includeDisabled?: boolean;
pluginIds?: readonly string[];
bundledChannelConfigCollector?: BundledChannelConfigCollector;
};
export type PluginRegistryContributionKey =
| "providers"
| "channels"
| "channelConfigs"
| "setupProviders"
| "cliBackends"
| "modelCatalogProviders"
| "commandAliases"
| "contracts";
type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & {
contribution: PluginRegistryContributionKey;
matches: string | ((contributionId: string) => boolean);
};
type ListPluginContributionIdsParams = PluginRegistryContributionOptions & {
contribution: PluginRegistryContributionKey;
};
type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & {
contract: PluginManifestContractListKey;
origin?: PluginOrigin;
onlyPluginIds?: readonly string[];
};
type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & {
contract: PluginManifestContractListKey;
value: string | undefined;
origin?: PluginOrigin;
};
function normalizeContributionId(value: string): string {
return value.trim();
}
function collectObjectKeys(value: Record<string, unknown> | undefined): readonly string[] {
return value ? Object.keys(value) : [];
}
function collectContractKeys(plugin: PluginManifestRecord): readonly string[] {
const contracts = plugin.contracts;
if (!contracts) {
return [];
}
return Object.entries(contracts).flatMap(([key, value]) =>
Array.isArray(value) && value.length > 0 ? [key] : [],
);
}
function listManifestContractValues(
plugin: PluginManifestRecord,
contract: PluginManifestContractListKey,
): readonly string[] {
return plugin.contracts?.[contract] ?? [];
}
function loadManifestContractRegistry(
params: LoadPluginRegistryParams & {
onlyPluginIds?: readonly string[];
},
): PluginManifestRegistry {
return loadPluginManifestRegistryForPluginRegistry({
...params,
pluginIds: params.onlyPluginIds,
includeDisabled: true,
});
}
function listManifestContributionIds(
plugin: PluginManifestRecord,
contribution: PluginRegistryContributionKey,
): readonly string[] {
switch (contribution) {
case "providers":
return plugin.providers;
case "channels":
return plugin.channels;
case "channelConfigs":
return collectObjectKeys(plugin.channelConfigs);
case "setupProviders":
return plugin.setup?.providers?.map((provider) => provider.id) ?? [];
case "cliBackends":
return [...plugin.cliBackends, ...(plugin.setup?.cliBackends ?? [])];
case "modelCatalogProviders":
return [
...collectObjectKeys(plugin.modelCatalog?.providers),
...collectObjectKeys(plugin.modelCatalog?.aliases),
];
case "commandAliases":
return plugin.commandAliases?.map((alias) => alias.name) ?? [];
case "contracts":
return collectContractKeys(plugin);
}
return [];
}
function resolveContributionPluginIds(params: {
index: PluginRegistrySnapshot;
includeDisabled?: boolean;
config?: OpenClawConfig;
}): readonly string[] {
if (params.includeDisabled) {
return params.index.plugins.map((plugin) => plugin.pluginId);
}
return params.index.plugins
.filter((plugin) => isInstalledPluginEnabled(params.index, plugin.pluginId, params.config))
.map((plugin) => plugin.pluginId);
}
function loadContributionManifestRegistry(
params: LoadPluginRegistryParams & {
index: PluginRegistrySnapshot;
includeDisabled?: boolean;
},
): PluginManifestRegistry {
return loadPluginManifestRegistryForInstalledIndex({
index: params.index,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
pluginIds: resolveContributionPluginIds({
index: params.index,
includeDisabled: params.includeDisabled,
config: params.config,
}),
includeDisabled: true,
});
}
function listContributionManifestPlugins(
params: PluginRegistryContributionOptions & {
index: PluginRegistrySnapshot;
},
): readonly PluginManifestRecord[] {
const plugins = params.lookUpTable?.plugins;
if (plugins) {
const enabledPluginIds = new Set(
resolveContributionPluginIds({
index: params.index,
includeDisabled: params.includeDisabled,
config: params.config,
}),
);
return plugins.filter((plugin) => enabledPluginIds.has(plugin.id));
}
return loadContributionManifestRegistry({
...params,
index: params.index,
}).plugins;
}
function resolveContributionOwnerMap(
table: PluginLookUpTable,
contribution: PluginRegistryContributionKey,
): ReadonlyMap<string, readonly string[]> | undefined {
switch (contribution) {
case "channels":
return table.owners.channels;
case "channelConfigs":
return table.owners.channelConfigs;
case "providers":
return table.owners.providers;
case "modelCatalogProviders":
return table.owners.modelCatalogProviders;
case "cliBackends":
return table.owners.cliBackends;
case "setupProviders":
return table.owners.setupProviders;
case "commandAliases":
return table.owners.commandAliases;
case "contracts":
return table.owners.contracts;
}
return undefined;
}
function filterContributionOwnerIds(params: {
owners: readonly string[];
index: PluginRegistrySnapshot;
includeDisabled?: boolean;
config?: OpenClawConfig;
}): readonly string[] {
const enabledPluginIds = new Set(
resolveContributionPluginIds({
index: params.index,
includeDisabled: params.includeDisabled,
config: params.config,
}),
);
return normalizeSortedUniqueStringEntries(
params.owners.filter((owner) => enabledPluginIds.has(owner)),
);
}
export function loadPluginManifestRegistryForPluginRegistry(
params: LoadPluginRegistryManifestParams = {},
): PluginManifestRegistry {
const { snapshot: index, manifestRegistry } = loadPluginRegistrySnapshotWithMetadata(params);
return loadPluginManifestRegistryForInstalledIndex({
index,
...(manifestRegistry ? { manifestRegistry } : {}),
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
pluginIds: params.pluginIds,
includeDisabled: params.includeDisabled,
...(params.bundledChannelConfigCollector
? { bundledChannelConfigCollector: params.bundledChannelConfigCollector }
: {}),
});
}
export function normalizePluginsConfigWithRegistry(
config: OpenClawConfig["plugins"] | undefined,
index: PluginRegistrySnapshot,
options: PluginRegistryIdNormalizerOptions = {},
): NormalizedPluginsConfig {
return normalizePluginsConfigWithResolver(
config,
createPluginRegistryIdNormalizer(index, options),
);
}
export function listPluginContributionIds(
params: ListPluginContributionIdsParams,
): readonly string[] {
const index = params.lookUpTable?.index ?? loadPluginRegistrySnapshot(params);
const plugins = listContributionManifestPlugins({ ...params, index });
return normalizeSortedUniqueStringEntries(
plugins.flatMap((plugin) => listManifestContributionIds(plugin, params.contribution)),
);
}
export function resolvePluginContributionOwners(
params: ResolvePluginContributionOwnersParams,
): readonly string[] {
const index = params.lookUpTable?.index ?? loadPluginRegistrySnapshot(params);
if (params.lookUpTable && typeof params.matches === "string") {
const ownerMap = resolveContributionOwnerMap(params.lookUpTable, params.contribution);
const owners = ownerMap?.get(params.matches);
if (owners) {
return filterContributionOwnerIds({
owners,
index,
includeDisabled: params.includeDisabled,
config: params.config,
});
}
return [];
}
const matcher =
typeof params.matches === "string"
? (contributionId: string) => contributionId === params.matches
: params.matches;
const plugins = listContributionManifestPlugins({ ...params, index });
return normalizeSortedUniqueStringEntries(
plugins.flatMap((plugin) =>
listManifestContributionIds(plugin, params.contribution).some(matcher) ? [plugin.id] : [],
),
);
}
export function resolveManifestContractPluginIds(
params: ResolveManifestContractPluginIdsParams,
): string[] {
return loadManifestContractRegistry(params)
.plugins.filter(
(plugin) =>
(!params.origin || plugin.origin === params.origin) &&
listManifestContractValues(plugin, params.contract).length > 0,
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
}
export function resolveManifestContractOwnerPluginId(
params: ResolveManifestContractOwnerPluginIdParams,
): string | undefined {
const normalizedValue = normalizeContributionId(params.value ?? "").toLowerCase();
if (!normalizedValue) {
return undefined;
}
return loadManifestContractRegistry(params).plugins.find(
(plugin) =>
(!params.origin || plugin.origin === params.origin) &&
listManifestContractValues(plugin, params.contract).some(
(candidate) => normalizeContributionId(candidate).toLowerCase() === normalizedValue,
),
)?.id;
}