mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 11:40:43 +00:00
583 lines
18 KiB
TypeScript
583 lines
18 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
|
import {
|
|
withBundledPluginAllowlistCompat,
|
|
withBundledPluginEnablementCompat,
|
|
withBundledPluginVitestCompat,
|
|
} from "./bundled-compat.js";
|
|
import {
|
|
resolveConfigScopedRuntimeCacheValue,
|
|
type ConfigScopedRuntimeCache,
|
|
} from "./plugin-cache-primitives.js";
|
|
import {
|
|
resolvePluginRegistryLoadCacheKey,
|
|
resolveRuntimePluginRegistry,
|
|
type PluginLoadOptions,
|
|
} from "./loader.js";
|
|
import {
|
|
hasManifestContractValue,
|
|
isManifestPluginAvailableForControlPlane,
|
|
loadManifestContractSnapshot,
|
|
listAvailableManifestContractValues,
|
|
} from "./manifest-contract-eligibility.js";
|
|
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
|
import type { PluginRegistry } from "./registry-types.js";
|
|
|
|
type CapabilityProviderRegistryKey =
|
|
| "memoryEmbeddingProviders"
|
|
| "speechProviders"
|
|
| "realtimeTranscriptionProviders"
|
|
| "realtimeVoiceProviders"
|
|
| "mediaUnderstandingProviders"
|
|
| "imageGenerationProviders"
|
|
| "videoGenerationProviders"
|
|
| "musicGenerationProviders";
|
|
|
|
type CapabilityContractKey =
|
|
| "memoryEmbeddingProviders"
|
|
| "speechProviders"
|
|
| "realtimeTranscriptionProviders"
|
|
| "realtimeVoiceProviders"
|
|
| "mediaUnderstandingProviders"
|
|
| "imageGenerationProviders"
|
|
| "videoGenerationProviders"
|
|
| "musicGenerationProviders";
|
|
|
|
type CapabilityProviderForKey<K extends CapabilityProviderRegistryKey> =
|
|
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
|
|
type CapabilityProviderEntries = PluginRegistry[CapabilityProviderRegistryKey];
|
|
type CapabilityPluginResolution = {
|
|
runtimePluginIds: string[];
|
|
bundledCompatPluginIds: string[];
|
|
};
|
|
|
|
const capabilityProviderSnapshotCache: ConfigScopedRuntimeCache<CapabilityProviderEntries> =
|
|
new WeakMap();
|
|
|
|
const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityContractKey> = {
|
|
memoryEmbeddingProviders: "memoryEmbeddingProviders",
|
|
speechProviders: "speechProviders",
|
|
realtimeTranscriptionProviders: "realtimeTranscriptionProviders",
|
|
realtimeVoiceProviders: "realtimeVoiceProviders",
|
|
mediaUnderstandingProviders: "mediaUnderstandingProviders",
|
|
imageGenerationProviders: "imageGenerationProviders",
|
|
videoGenerationProviders: "videoGenerationProviders",
|
|
musicGenerationProviders: "musicGenerationProviders",
|
|
};
|
|
|
|
function shouldResolveWhenPluginsAreGloballyDisabled(key: CapabilityProviderRegistryKey): boolean {
|
|
return key === "speechProviders";
|
|
}
|
|
|
|
function shouldSkipCapabilityResolution(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
}): boolean {
|
|
return (
|
|
params.cfg?.plugins?.enabled === false &&
|
|
!shouldResolveWhenPluginsAreGloballyDisabled(params.key)
|
|
);
|
|
}
|
|
|
|
function uniqueSorted(values: Iterable<string>): string[] {
|
|
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
export function loadCapabilityManifestSnapshot(params: {
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {
|
|
return loadManifestContractSnapshot({
|
|
config: params.cfg,
|
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
|
});
|
|
}
|
|
|
|
function resolveCapabilityPluginIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
providerId?: string;
|
|
}): CapabilityPluginResolution {
|
|
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
|
const snapshot = loadCapabilityManifestSnapshot(params);
|
|
const contractPlugins = snapshot.plugins.filter((plugin) =>
|
|
hasManifestContractValue({
|
|
plugin,
|
|
contract: contractKey,
|
|
value: params.providerId,
|
|
}),
|
|
);
|
|
return {
|
|
runtimePluginIds: uniqueSorted(
|
|
contractPlugins
|
|
.filter((plugin) =>
|
|
isManifestPluginAvailableForControlPlane({
|
|
snapshot,
|
|
plugin,
|
|
config: params.cfg,
|
|
}),
|
|
)
|
|
.map((plugin) => plugin.id),
|
|
),
|
|
bundledCompatPluginIds: uniqueSorted(
|
|
contractPlugins.filter((plugin) => plugin.origin === "bundled").map((plugin) => plugin.id),
|
|
),
|
|
};
|
|
}
|
|
|
|
function resolveBundledCapabilityCompatPluginIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
providerId?: string;
|
|
}): string[] {
|
|
return resolveCapabilityPluginIds(params).bundledCompatPluginIds;
|
|
}
|
|
|
|
export function resolveManifestCapabilityProviderIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
}): string[] {
|
|
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
|
return listAvailableManifestContractValues({
|
|
snapshot: loadCapabilityManifestSnapshot(params),
|
|
contract: contractKey,
|
|
config: params.cfg,
|
|
});
|
|
}
|
|
|
|
export function resolveBundledCapabilityProviderIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
}): string[] {
|
|
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
|
const snapshot = loadCapabilityManifestSnapshot(params);
|
|
return uniqueSorted(
|
|
snapshot.plugins.flatMap((plugin) =>
|
|
plugin.origin === "bundled" ? (plugin.contracts?.[contractKey] ?? []) : [],
|
|
),
|
|
);
|
|
}
|
|
|
|
function resolveCapabilityProviderConfig(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
pluginIds?: string[];
|
|
}) {
|
|
const pluginIds = params.pluginIds ?? resolveBundledCapabilityCompatPluginIds(params);
|
|
const allowlistCompat = withBundledPluginAllowlistCompat({
|
|
config: params.cfg,
|
|
pluginIds,
|
|
});
|
|
const enablementCompat = withBundledPluginEnablementCompat({
|
|
config: allowlistCompat,
|
|
pluginIds,
|
|
});
|
|
return withBundledPluginVitestCompat({
|
|
config: enablementCompat,
|
|
pluginIds,
|
|
env: process.env,
|
|
});
|
|
}
|
|
|
|
function createCapabilityProviderFallbackLoadOptions(params: {
|
|
compatConfig?: OpenClawConfig;
|
|
pluginIds: string[];
|
|
}): PluginLoadOptions {
|
|
return {
|
|
...(params.compatConfig === undefined ? {} : { config: params.compatConfig }),
|
|
onlyPluginIds: params.pluginIds,
|
|
activate: false,
|
|
};
|
|
}
|
|
|
|
function resolveCapabilityProviderSnapshotCacheKey(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
loadOptions: PluginLoadOptions;
|
|
}): string {
|
|
return JSON.stringify({
|
|
key: params.key,
|
|
load: resolvePluginRegistryLoadCacheKey(params.loadOptions),
|
|
});
|
|
}
|
|
|
|
function findProviderById<K extends CapabilityProviderRegistryKey>(
|
|
entries: PluginRegistry[K],
|
|
providerId: string,
|
|
): CapabilityProviderForKey<K> | undefined {
|
|
const providerEntries = entries as unknown as Array<{
|
|
provider: CapabilityProviderForKey<K> & { id?: unknown };
|
|
}>;
|
|
for (const entry of providerEntries) {
|
|
if (entry.provider.id === providerId) {
|
|
return entry.provider;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function mergeCapabilityProviders<K extends CapabilityProviderRegistryKey>(
|
|
left: PluginRegistry[K],
|
|
right: PluginRegistry[K],
|
|
): CapabilityProviderForKey<K>[] {
|
|
const merged = new Map<string, CapabilityProviderForKey<K>>();
|
|
const unnamed: CapabilityProviderForKey<K>[] = [];
|
|
const addEntries = (entries: PluginRegistry[K]) => {
|
|
for (const entry of entries) {
|
|
const provider = entry.provider as CapabilityProviderForKey<K> & { id?: string };
|
|
if (!provider.id) {
|
|
unnamed.push(provider);
|
|
continue;
|
|
}
|
|
if (!merged.has(provider.id)) {
|
|
merged.set(provider.id, provider);
|
|
}
|
|
}
|
|
};
|
|
|
|
addEntries(left);
|
|
addEntries(right);
|
|
return [...merged.values(), ...unnamed];
|
|
}
|
|
|
|
function mergeCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(
|
|
left: PluginRegistry[K],
|
|
right: PluginRegistry[K],
|
|
): PluginRegistry[K] {
|
|
const merged = new Map<string, PluginRegistry[K][number]>();
|
|
const unnamed: Array<PluginRegistry[K][number]> = [];
|
|
const addEntries = (entries: PluginRegistry[K]) => {
|
|
for (const entry of entries) {
|
|
const provider = entry.provider as { id?: string };
|
|
if (!provider.id) {
|
|
unnamed.push(entry);
|
|
continue;
|
|
}
|
|
if (!merged.has(provider.id)) {
|
|
merged.set(provider.id, entry);
|
|
}
|
|
}
|
|
};
|
|
|
|
addEntries(left);
|
|
addEntries(right);
|
|
return [...merged.values(), ...unnamed] as PluginRegistry[K];
|
|
}
|
|
|
|
function addObjectKeys(target: Set<string>, value: unknown): void {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
return;
|
|
}
|
|
for (const key of Object.keys(value)) {
|
|
const normalized = key.trim().toLowerCase();
|
|
if (normalized) {
|
|
target.add(normalized);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addStringValue(target: Set<string>, value: unknown): void {
|
|
if (typeof value !== "string") {
|
|
return;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized) {
|
|
target.add(normalized);
|
|
}
|
|
}
|
|
|
|
function collectRequestedSpeechProviderIds(cfg: OpenClawConfig | undefined): Set<string> {
|
|
const requested = new Set<string>();
|
|
const tts =
|
|
typeof cfg?.messages?.tts === "object" && cfg.messages.tts !== null
|
|
? (cfg.messages.tts as Record<string, unknown>)
|
|
: undefined;
|
|
addStringValue(requested, tts?.provider);
|
|
addObjectKeys(requested, tts?.providers);
|
|
addObjectKeys(requested, cfg?.models?.providers);
|
|
return requested;
|
|
}
|
|
|
|
function addMediaModelProviders(target: Set<string>, value: unknown): void {
|
|
if (!Array.isArray(value)) {
|
|
return;
|
|
}
|
|
for (const entry of value) {
|
|
if (typeof entry === "object" && entry !== null) {
|
|
addStringValue(target, (entry as { provider?: unknown }).provider);
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectRequestedMediaUnderstandingProviderIds(
|
|
cfg: OpenClawConfig | undefined,
|
|
): Set<string> {
|
|
const requested = new Set<string>();
|
|
const media = cfg?.tools?.media;
|
|
addMediaModelProviders(requested, media?.models);
|
|
addMediaModelProviders(requested, media?.image?.models);
|
|
addMediaModelProviders(requested, media?.audio?.models);
|
|
addMediaModelProviders(requested, media?.video?.models);
|
|
return requested;
|
|
}
|
|
|
|
function collectRequestedCapabilityProviderIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
}): Set<string> | undefined {
|
|
switch (params.key) {
|
|
case "speechProviders":
|
|
return collectRequestedSpeechProviderIds(params.cfg);
|
|
case "mediaUnderstandingProviders":
|
|
return collectRequestedMediaUnderstandingProviderIds(params.cfg);
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function removeActiveProviderIds(requested: Set<string>, entries: readonly unknown[]): void {
|
|
for (const entry of entries as Array<{ provider: { id?: unknown; aliases?: unknown } }>) {
|
|
const provider = entry.provider as { id?: unknown; aliases?: unknown };
|
|
if (typeof provider.id === "string") {
|
|
requested.delete(provider.id.toLowerCase());
|
|
}
|
|
if (Array.isArray(provider.aliases)) {
|
|
for (const alias of provider.aliases) {
|
|
if (typeof alias === "string") {
|
|
requested.delete(alias.toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function filterLoadedProvidersForRequestedConfig<K extends CapabilityProviderRegistryKey>(params: {
|
|
key: K;
|
|
requested: Set<string>;
|
|
entries: PluginRegistry[K];
|
|
}): PluginRegistry[K] {
|
|
if (params.key !== "speechProviders" && params.key !== "mediaUnderstandingProviders") {
|
|
return [] as unknown as PluginRegistry[K];
|
|
}
|
|
if (params.requested.size === 0) {
|
|
return [] as unknown as PluginRegistry[K];
|
|
}
|
|
return params.entries.filter((entry) => {
|
|
const provider = entry.provider as { id?: unknown; aliases?: unknown };
|
|
if (typeof provider.id === "string" && params.requested.has(provider.id.toLowerCase())) {
|
|
return true;
|
|
}
|
|
if (Array.isArray(provider.aliases)) {
|
|
return provider.aliases.some(
|
|
(alias) => typeof alias === "string" && params.requested.has(alias.toLowerCase()),
|
|
);
|
|
}
|
|
return false;
|
|
}) as PluginRegistry[K];
|
|
}
|
|
|
|
function resolveRequestedCapabilityPluginIds(params: {
|
|
key: CapabilityProviderRegistryKey;
|
|
cfg?: OpenClawConfig;
|
|
requested?: Set<string>;
|
|
}): CapabilityPluginResolution | undefined {
|
|
if (params.key !== "speechProviders" || !params.requested || params.requested.size === 0) {
|
|
return undefined;
|
|
}
|
|
const runtimePluginIds = new Set<string>();
|
|
const bundledCompatPluginIds = new Set<string>();
|
|
for (const providerId of params.requested) {
|
|
const resolution = resolveCapabilityPluginIds({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
providerId,
|
|
});
|
|
for (const pluginId of resolution.runtimePluginIds) {
|
|
runtimePluginIds.add(pluginId);
|
|
}
|
|
for (const pluginId of resolution.bundledCompatPluginIds) {
|
|
bundledCompatPluginIds.add(pluginId);
|
|
}
|
|
}
|
|
return runtimePluginIds.size > 0
|
|
? {
|
|
runtimePluginIds: uniqueSorted(runtimePluginIds),
|
|
bundledCompatPluginIds: uniqueSorted(bundledCompatPluginIds),
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
function loadCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(params: {
|
|
key: K;
|
|
bundledCompatPluginIds: string[];
|
|
loadOptions: PluginLoadOptions;
|
|
requested?: Set<string>;
|
|
}): PluginRegistry[K] {
|
|
const registry = resolveRuntimePluginRegistry(params.loadOptions);
|
|
const entries = registry?.[params.key] ?? [];
|
|
const missingRequested =
|
|
params.key === "speechProviders" && params.requested && params.requested.size > 0
|
|
? new Set(params.requested)
|
|
: undefined;
|
|
if (missingRequested) {
|
|
removeActiveProviderIds(missingRequested, entries);
|
|
}
|
|
if (entries.length > 0 && (!missingRequested || missingRequested.size === 0)) {
|
|
return entries;
|
|
}
|
|
if (params.bundledCompatPluginIds.length === 0) {
|
|
return entries;
|
|
}
|
|
const captured = loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: params.bundledCompatPluginIds,
|
|
env: process.env,
|
|
pluginSdkResolution: params.loadOptions.pluginSdkResolution,
|
|
})[params.key] as PluginRegistry[K];
|
|
return entries.length > 0 ? mergeCapabilityProviderEntries(entries, captured) : captured;
|
|
}
|
|
|
|
export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegistryKey>(params: {
|
|
key: K;
|
|
providerId: string;
|
|
cfg?: OpenClawConfig;
|
|
}): CapabilityProviderForKey<K> | undefined {
|
|
if (shouldSkipCapabilityResolution(params)) {
|
|
return undefined;
|
|
}
|
|
|
|
const activeRegistry = resolveRuntimePluginRegistry();
|
|
const activeProvider = findProviderById(activeRegistry?.[params.key] ?? [], params.providerId);
|
|
if (activeProvider) {
|
|
return activeProvider;
|
|
}
|
|
|
|
const pluginIds = resolveCapabilityPluginIds({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
providerId: params.providerId,
|
|
});
|
|
if (pluginIds.runtimePluginIds.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const compatConfig = resolveCapabilityProviderConfig({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
pluginIds: pluginIds.bundledCompatPluginIds,
|
|
});
|
|
const loadOptions = createCapabilityProviderFallbackLoadOptions({
|
|
compatConfig,
|
|
pluginIds: pluginIds.runtimePluginIds,
|
|
});
|
|
const loadedProviders = resolveConfigScopedRuntimeCacheValue({
|
|
cache: capabilityProviderSnapshotCache,
|
|
config: params.cfg,
|
|
key: resolveCapabilityProviderSnapshotCacheKey({ key: params.key, loadOptions }),
|
|
load: () =>
|
|
loadCapabilityProviderEntries({
|
|
key: params.key,
|
|
bundledCompatPluginIds: pluginIds.bundledCompatPluginIds,
|
|
loadOptions,
|
|
requested: new Set([params.providerId.toLowerCase()]),
|
|
}) as CapabilityProviderEntries,
|
|
}) as PluginRegistry[K];
|
|
return findProviderById(loadedProviders, params.providerId);
|
|
}
|
|
|
|
function resolveCachedCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(params: {
|
|
key: K;
|
|
cfg?: OpenClawConfig;
|
|
bundledCompatPluginIds: string[];
|
|
loadOptions: PluginLoadOptions;
|
|
requested?: Set<string>;
|
|
}): PluginRegistry[K] {
|
|
return resolveConfigScopedRuntimeCacheValue({
|
|
cache: capabilityProviderSnapshotCache,
|
|
config: params.cfg,
|
|
key: resolveCapabilityProviderSnapshotCacheKey({
|
|
key: params.key,
|
|
loadOptions: params.loadOptions,
|
|
}),
|
|
load: () =>
|
|
loadCapabilityProviderEntries({
|
|
key: params.key,
|
|
bundledCompatPluginIds: params.bundledCompatPluginIds,
|
|
loadOptions: params.loadOptions,
|
|
requested: params.requested,
|
|
}) as CapabilityProviderEntries,
|
|
}) as PluginRegistry[K];
|
|
}
|
|
|
|
export function resolvePluginCapabilityProviders<K extends CapabilityProviderRegistryKey>(params: {
|
|
key: K;
|
|
cfg?: OpenClawConfig;
|
|
}): CapabilityProviderForKey<K>[] {
|
|
if (shouldSkipCapabilityResolution(params)) {
|
|
return [];
|
|
}
|
|
|
|
const activeRegistry = resolveRuntimePluginRegistry();
|
|
const activeProviders = activeRegistry?.[params.key] ?? [];
|
|
const missingRequestedProviders =
|
|
activeProviders.length > 0
|
|
? collectRequestedCapabilityProviderIds({ key: params.key, cfg: params.cfg })
|
|
: undefined;
|
|
if (activeProviders.length > 0 && params.key !== "memoryEmbeddingProviders") {
|
|
if (!missingRequestedProviders) {
|
|
return activeProviders.map((entry) => entry.provider) as CapabilityProviderForKey<K>[];
|
|
}
|
|
removeActiveProviderIds(missingRequestedProviders, activeProviders);
|
|
if (missingRequestedProviders.size === 0) {
|
|
return activeProviders.map((entry) => entry.provider) as CapabilityProviderForKey<K>[];
|
|
}
|
|
}
|
|
let requestedSpeechProviders: Set<string> | undefined;
|
|
if (params.key === "speechProviders") {
|
|
requestedSpeechProviders =
|
|
missingRequestedProviders ??
|
|
(activeProviders.length === 0 ? collectRequestedSpeechProviderIds(params.cfg) : undefined);
|
|
}
|
|
const pluginIds =
|
|
resolveRequestedCapabilityPluginIds({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
requested: requestedSpeechProviders,
|
|
}) ??
|
|
resolveCapabilityPluginIds({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
});
|
|
const compatConfig = resolveCapabilityProviderConfig({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
pluginIds: pluginIds.bundledCompatPluginIds,
|
|
});
|
|
const loadOptions = createCapabilityProviderFallbackLoadOptions({
|
|
compatConfig,
|
|
pluginIds: pluginIds.runtimePluginIds,
|
|
});
|
|
const loadedProviders = resolveCachedCapabilityProviderEntries({
|
|
key: params.key,
|
|
cfg: params.cfg,
|
|
bundledCompatPluginIds: pluginIds.bundledCompatPluginIds,
|
|
loadOptions,
|
|
requested: requestedSpeechProviders,
|
|
});
|
|
if (params.key !== "memoryEmbeddingProviders") {
|
|
const mergeLoadedProviders =
|
|
activeProviders.length > 0
|
|
? filterLoadedProvidersForRequestedConfig({
|
|
key: params.key,
|
|
requested: missingRequestedProviders ?? new Set(),
|
|
entries: loadedProviders,
|
|
})
|
|
: loadedProviders;
|
|
return mergeCapabilityProviders(activeProviders, mergeLoadedProviders);
|
|
}
|
|
return mergeCapabilityProviders(activeProviders, loadedProviders);
|
|
}
|