mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 08:02:04 +00:00
469 lines
16 KiB
TypeScript
469 lines
16 KiB
TypeScript
import {
|
|
BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
|
BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
|
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS,
|
|
BUNDLED_PROVIDER_PLUGIN_IDS,
|
|
BUNDLED_SPEECH_PLUGIN_IDS,
|
|
BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
|
} from "../bundled-capability-metadata.js";
|
|
import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js";
|
|
import type {
|
|
ImageGenerationProviderPlugin,
|
|
MediaUnderstandingProviderPlugin,
|
|
ProviderPlugin,
|
|
SpeechProviderPlugin,
|
|
WebSearchProviderPlugin,
|
|
} from "../types.js";
|
|
import {
|
|
loadVitestImageGenerationProviderContractRegistry,
|
|
loadVitestMediaUnderstandingProviderContractRegistry,
|
|
loadVitestSpeechProviderContractRegistry,
|
|
} from "./speech-vitest-registry.js";
|
|
|
|
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
|
|
type CapabilityContractEntry<T> = {
|
|
pluginId: string;
|
|
provider: T;
|
|
};
|
|
|
|
type ProviderContractEntry = CapabilityContractEntry<ProviderPlugin>;
|
|
|
|
type WebSearchProviderContractEntry = CapabilityContractEntry<WebSearchProviderPlugin> & {
|
|
credentialValue: unknown;
|
|
};
|
|
|
|
type SpeechProviderContractEntry = CapabilityContractEntry<SpeechProviderPlugin>;
|
|
type MediaUnderstandingProviderContractEntry =
|
|
CapabilityContractEntry<MediaUnderstandingProviderPlugin>;
|
|
type ImageGenerationProviderContractEntry = CapabilityContractEntry<ImageGenerationProviderPlugin>;
|
|
|
|
type PluginRegistrationContractEntry = {
|
|
pluginId: string;
|
|
cliBackendIds: string[];
|
|
providerIds: string[];
|
|
speechProviderIds: string[];
|
|
mediaUnderstandingProviderIds: string[];
|
|
imageGenerationProviderIds: string[];
|
|
webSearchProviderIds: string[];
|
|
toolNames: string[];
|
|
};
|
|
|
|
function createProviderContractPluginIdsByProviderId(): Map<string, string[]> {
|
|
const result = new Map<string, string[]>();
|
|
for (const entry of BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS) {
|
|
for (const providerId of entry.providerIds) {
|
|
const existing = result.get(providerId) ?? [];
|
|
if (!existing.includes(entry.pluginId)) {
|
|
existing.push(entry.pluginId);
|
|
}
|
|
result.set(providerId, existing);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function uniqueStrings(values: readonly string[]): string[] {
|
|
const result: string[] = [];
|
|
const seen = new Set<string>();
|
|
for (const value of values) {
|
|
if (seen.has(value)) {
|
|
continue;
|
|
}
|
|
seen.add(value);
|
|
result.push(value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
let providerContractRegistryCache: ProviderContractEntry[] | null = null;
|
|
let providerContractRegistryByPluginIdCache: Map<string, ProviderContractEntry[]> | null = null;
|
|
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
|
|
let webSearchProviderContractRegistryByPluginIdCache: Map<
|
|
string,
|
|
WebSearchProviderContractEntry[]
|
|
> | null = null;
|
|
let speechProviderContractRegistryCache: SpeechProviderContractEntry[] | null = null;
|
|
let mediaUnderstandingProviderContractRegistryCache:
|
|
| MediaUnderstandingProviderContractEntry[]
|
|
| null = null;
|
|
let imageGenerationProviderContractRegistryCache: ImageGenerationProviderContractEntry[] | null =
|
|
null;
|
|
const providerContractPluginIdsByProviderId = createProviderContractPluginIdsByProviderId();
|
|
|
|
export let providerContractLoadError: Error | undefined;
|
|
|
|
function formatBundledCapabilityPluginLoadError(params: {
|
|
pluginId: string;
|
|
capabilityLabel: string;
|
|
registry: BundledCapabilityRuntimeRegistry;
|
|
}): Error {
|
|
const plugin = params.registry.plugins.find((entry) => entry.id === params.pluginId);
|
|
const diagnostics = params.registry.diagnostics
|
|
.filter((entry) => entry.pluginId === params.pluginId)
|
|
.map((entry) => entry.message);
|
|
const detailParts = plugin
|
|
? [
|
|
`status=${plugin.status}`,
|
|
...(plugin.error ? [`error=${plugin.error}`] : []),
|
|
`providerIds=[${plugin.providerIds.join(", ")}]`,
|
|
`webSearchProviderIds=[${plugin.webSearchProviderIds.join(", ")}]`,
|
|
]
|
|
: ["plugin record missing"];
|
|
if (diagnostics.length > 0) {
|
|
detailParts.push(`diagnostics=${diagnostics.join(" | ")}`);
|
|
}
|
|
return new Error(
|
|
`bundled ${params.capabilityLabel} contract load failed for ${params.pluginId}: ${detailParts.join("; ")}`,
|
|
);
|
|
}
|
|
|
|
function loadScopedCapabilityRuntimeRegistryEntries<T>(params: {
|
|
pluginId: string;
|
|
capabilityLabel: string;
|
|
loadEntries: (registry: BundledCapabilityRuntimeRegistry) => T[];
|
|
loadDeclaredIds: (
|
|
plugin: BundledCapabilityRuntimeRegistry["plugins"][number],
|
|
) => readonly string[];
|
|
}): T[] {
|
|
let lastFailure: Error | undefined;
|
|
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const registry = loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: [params.pluginId],
|
|
pluginSdkResolution: "dist",
|
|
});
|
|
const entries = params.loadEntries(registry);
|
|
if (entries.length > 0) {
|
|
return entries;
|
|
}
|
|
|
|
const plugin = registry.plugins.find((entry) => entry.id === params.pluginId);
|
|
lastFailure = formatBundledCapabilityPluginLoadError({
|
|
pluginId: params.pluginId,
|
|
capabilityLabel: params.capabilityLabel,
|
|
registry,
|
|
});
|
|
const shouldRetry =
|
|
attempt === 0 &&
|
|
(!plugin || plugin.status !== "loaded" || params.loadDeclaredIds(plugin).length === 0);
|
|
if (!shouldRetry) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
throw (
|
|
lastFailure ??
|
|
new Error(
|
|
`bundled ${params.capabilityLabel} contract load failed for ${params.pluginId}: no entries`,
|
|
)
|
|
);
|
|
}
|
|
|
|
function loadProviderContractEntriesForPluginIds(
|
|
pluginIds: readonly string[],
|
|
): ProviderContractEntry[] {
|
|
return pluginIds.flatMap((pluginId) => loadProviderContractEntriesForPluginId(pluginId));
|
|
}
|
|
|
|
function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContractEntry[] {
|
|
if (providerContractRegistryCache) {
|
|
return providerContractRegistryCache.filter((entry) => entry.pluginId === pluginId);
|
|
}
|
|
|
|
const cache =
|
|
providerContractRegistryByPluginIdCache ?? new Map<string, ProviderContractEntry[]>();
|
|
providerContractRegistryByPluginIdCache = cache;
|
|
const cached = cache.get(pluginId);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
try {
|
|
providerContractLoadError = undefined;
|
|
const entries = loadScopedCapabilityRuntimeRegistryEntries({
|
|
pluginId,
|
|
capabilityLabel: "provider",
|
|
loadEntries: (registry) =>
|
|
registry.providers
|
|
.filter((entry) => entry.pluginId === pluginId)
|
|
.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
})),
|
|
loadDeclaredIds: (plugin) => plugin.providerIds,
|
|
}).map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
}));
|
|
cache.set(pluginId, entries);
|
|
return entries;
|
|
} catch (error) {
|
|
providerContractLoadError = error instanceof Error ? error : new Error(String(error));
|
|
cache.set(pluginId, []);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function loadProviderContractRegistry(): ProviderContractEntry[] {
|
|
if (!providerContractRegistryCache) {
|
|
try {
|
|
providerContractLoadError = undefined;
|
|
providerContractRegistryCache = loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: BUNDLED_PROVIDER_PLUGIN_IDS,
|
|
pluginSdkResolution: "dist",
|
|
}).providers.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
}));
|
|
} catch (error) {
|
|
providerContractLoadError = error instanceof Error ? error : new Error(String(error));
|
|
providerContractRegistryCache = [];
|
|
}
|
|
}
|
|
return providerContractRegistryCache;
|
|
}
|
|
|
|
function loadUniqueProviderContractProviders(): ProviderPlugin[] {
|
|
return [
|
|
...new Map(
|
|
loadProviderContractRegistry().map((entry) => [entry.provider.id, entry.provider]),
|
|
).values(),
|
|
];
|
|
}
|
|
|
|
function loadProviderContractPluginIds(): string[] {
|
|
return [...BUNDLED_PROVIDER_PLUGIN_IDS];
|
|
}
|
|
|
|
function loadProviderContractCompatPluginIds(): string[] {
|
|
return loadProviderContractPluginIds();
|
|
}
|
|
|
|
function resolveWebSearchCredentialValue(provider: WebSearchProviderPlugin): unknown {
|
|
if (provider.requiresCredential === false) {
|
|
return `${provider.id}-no-key-needed`;
|
|
}
|
|
const envVar = provider.envVars.find((entry) => entry.trim().length > 0);
|
|
if (!envVar) {
|
|
return `${provider.id}-test`;
|
|
}
|
|
if (envVar === "OPENROUTER_API_KEY") {
|
|
return "openrouter-test";
|
|
}
|
|
return envVar.toLowerCase().includes("api_key") ? `${provider.id}-test` : "sk-test";
|
|
}
|
|
|
|
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
|
if (!webSearchProviderContractRegistryCache) {
|
|
const registry = loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
|
pluginSdkResolution: "dist",
|
|
});
|
|
webSearchProviderContractRegistryCache = registry.webSearchProviders.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
|
}));
|
|
}
|
|
return webSearchProviderContractRegistryCache;
|
|
}
|
|
|
|
export function resolveWebSearchProviderContractEntriesForPluginId(
|
|
pluginId: string,
|
|
): WebSearchProviderContractEntry[] {
|
|
if (webSearchProviderContractRegistryCache) {
|
|
return webSearchProviderContractRegistryCache.filter((entry) => entry.pluginId === pluginId);
|
|
}
|
|
|
|
const cache =
|
|
webSearchProviderContractRegistryByPluginIdCache ??
|
|
new Map<string, WebSearchProviderContractEntry[]>();
|
|
webSearchProviderContractRegistryByPluginIdCache = cache;
|
|
const cached = cache.get(pluginId);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const entries = loadScopedCapabilityRuntimeRegistryEntries({
|
|
pluginId,
|
|
capabilityLabel: "web search provider",
|
|
loadEntries: (registry) =>
|
|
registry.webSearchProviders
|
|
.filter((entry) => entry.pluginId === pluginId)
|
|
.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
|
})),
|
|
loadDeclaredIds: (plugin) => plugin.webSearchProviderIds,
|
|
});
|
|
cache.set(pluginId, entries);
|
|
return entries;
|
|
}
|
|
|
|
function loadSpeechProviderContractRegistry(): SpeechProviderContractEntry[] {
|
|
if (!speechProviderContractRegistryCache) {
|
|
speechProviderContractRegistryCache = process.env.VITEST
|
|
? loadVitestSpeechProviderContractRegistry()
|
|
: loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: BUNDLED_SPEECH_PLUGIN_IDS,
|
|
pluginSdkResolution: "dist",
|
|
}).speechProviders.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
}));
|
|
}
|
|
return speechProviderContractRegistryCache;
|
|
}
|
|
|
|
function loadMediaUnderstandingProviderContractRegistry(): MediaUnderstandingProviderContractEntry[] {
|
|
if (!mediaUnderstandingProviderContractRegistryCache) {
|
|
mediaUnderstandingProviderContractRegistryCache = process.env.VITEST
|
|
? loadVitestMediaUnderstandingProviderContractRegistry()
|
|
: loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
|
pluginSdkResolution: "dist",
|
|
}).mediaUnderstandingProviders.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
}));
|
|
}
|
|
return mediaUnderstandingProviderContractRegistryCache;
|
|
}
|
|
|
|
function loadImageGenerationProviderContractRegistry(): ImageGenerationProviderContractEntry[] {
|
|
if (!imageGenerationProviderContractRegistryCache) {
|
|
imageGenerationProviderContractRegistryCache = process.env.VITEST
|
|
? loadVitestImageGenerationProviderContractRegistry()
|
|
: loadBundledCapabilityRuntimeRegistry({
|
|
pluginIds: BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
|
pluginSdkResolution: "dist",
|
|
}).imageGenerationProviders.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
provider: entry.provider,
|
|
}));
|
|
}
|
|
return imageGenerationProviderContractRegistryCache;
|
|
}
|
|
|
|
function createLazyArrayView<T>(load: () => T[]): T[] {
|
|
return new Proxy([] as T[], {
|
|
get(_target, prop) {
|
|
const actual = load();
|
|
const value = Reflect.get(actual, prop, actual);
|
|
return typeof value === "function" ? value.bind(actual) : value;
|
|
},
|
|
has(_target, prop) {
|
|
return Reflect.has(load(), prop);
|
|
},
|
|
ownKeys() {
|
|
return Reflect.ownKeys(load());
|
|
},
|
|
getOwnPropertyDescriptor(_target, prop) {
|
|
const actual = load();
|
|
const descriptor = Reflect.getOwnPropertyDescriptor(actual, prop);
|
|
if (descriptor) {
|
|
return descriptor;
|
|
}
|
|
if (Reflect.has(actual, prop)) {
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: false,
|
|
value: Reflect.get(actual, prop, actual),
|
|
};
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
}
|
|
|
|
export const providerContractRegistry: ProviderContractEntry[] = createLazyArrayView(
|
|
loadProviderContractRegistry,
|
|
);
|
|
|
|
export const uniqueProviderContractProviders: ProviderPlugin[] = createLazyArrayView(
|
|
loadUniqueProviderContractProviders,
|
|
);
|
|
|
|
export const providerContractPluginIds: string[] = createLazyArrayView(
|
|
loadProviderContractPluginIds,
|
|
);
|
|
|
|
export const providerContractCompatPluginIds: string[] = createLazyArrayView(
|
|
loadProviderContractCompatPluginIds,
|
|
);
|
|
|
|
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
|
|
const pluginIds = providerContractPluginIdsByProviderId.get(providerId) ?? [];
|
|
const entries = loadProviderContractEntriesForPluginIds(pluginIds);
|
|
const provider = entries.find((entry) => entry.provider.id === providerId)?.provider;
|
|
if (!provider) {
|
|
const pluginScopedProviders = [
|
|
...new Map(entries.map((entry) => [entry.provider.id, entry.provider])).values(),
|
|
];
|
|
// Paired catalogs may expose multiple runtime provider ids from one shared
|
|
// ProviderPlugin contract entry. Reuse that single contract surface for the
|
|
// manifest-owned alias ids instead of requiring duplicate registration.
|
|
if (pluginIds.length === 1 && pluginScopedProviders.length === 1) {
|
|
return pluginScopedProviders[0];
|
|
}
|
|
if (providerContractLoadError) {
|
|
throw new Error(
|
|
`provider contract entry missing for ${providerId}; bundled provider registry failed to load: ${providerContractLoadError.message}`,
|
|
);
|
|
}
|
|
throw new Error(`provider contract entry missing for ${providerId}`);
|
|
}
|
|
return provider;
|
|
}
|
|
|
|
export function resolveProviderContractPluginIdsForProvider(
|
|
providerId: string,
|
|
): string[] | undefined {
|
|
const pluginIds = providerContractPluginIdsByProviderId.get(providerId) ?? [];
|
|
return pluginIds.length > 0 ? pluginIds : undefined;
|
|
}
|
|
|
|
export function resolveProviderContractProvidersForPluginIds(
|
|
pluginIds: readonly string[],
|
|
): ProviderPlugin[] {
|
|
const allowed = new Set(pluginIds);
|
|
return [
|
|
...new Map(
|
|
loadProviderContractEntriesForPluginIds([...allowed])
|
|
.filter((entry) => allowed.has(entry.pluginId))
|
|
.map((entry) => [entry.provider.id, entry.provider]),
|
|
).values(),
|
|
];
|
|
}
|
|
|
|
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
|
|
createLazyArrayView(loadWebSearchProviderContractRegistry);
|
|
|
|
export const speechProviderContractRegistry: SpeechProviderContractEntry[] = createLazyArrayView(
|
|
loadSpeechProviderContractRegistry,
|
|
);
|
|
|
|
export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProviderContractEntry[] =
|
|
createLazyArrayView(loadMediaUnderstandingProviderContractRegistry);
|
|
|
|
export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] =
|
|
createLazyArrayView(loadImageGenerationProviderContractRegistry);
|
|
|
|
function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEntry[] {
|
|
return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
cliBackendIds: uniqueStrings(entry.cliBackendIds),
|
|
providerIds: uniqueStrings(entry.providerIds),
|
|
speechProviderIds: uniqueStrings(entry.speechProviderIds),
|
|
mediaUnderstandingProviderIds: uniqueStrings(entry.mediaUnderstandingProviderIds),
|
|
imageGenerationProviderIds: uniqueStrings(entry.imageGenerationProviderIds),
|
|
webSearchProviderIds: uniqueStrings(entry.webSearchProviderIds),
|
|
toolNames: uniqueStrings(entry.toolNames),
|
|
}));
|
|
}
|
|
|
|
export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] =
|
|
createLazyArrayView(loadPluginRegistrationContractRegistry);
|