Files
openclaw/src/plugins/web-provider-resolution-shared.ts
2026-04-06 15:26:32 +01:00

191 lines
6.1 KiB
TypeScript

import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
import type { NormalizedPluginsConfig } from "./config-state.js";
import type { PluginLoadOptions } from "./loader.js";
import {
loadPluginManifestRegistry,
resolveManifestContractPluginIds,
type PluginManifestRecord,
} from "./manifest-registry.js";
export type WebProviderContract = "webSearchProviders" | "webFetchProviders";
export type WebProviderConfigKey = "webSearch" | "webFetch";
type WebProviderSortEntry = {
id: string;
pluginId: string;
autoDetectOrder?: number;
};
function comparePluginProvidersAlphabetically(
left: Pick<WebProviderSortEntry, "id" | "pluginId">,
right: Pick<WebProviderSortEntry, "id" | "pluginId">,
): number {
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
}
export function sortPluginProviders<T extends Pick<WebProviderSortEntry, "id" | "pluginId">>(
providers: T[],
): T[] {
return providers.toSorted(comparePluginProvidersAlphabetically);
}
export function sortPluginProvidersForAutoDetect<T extends WebProviderSortEntry>(
providers: T[],
): T[] {
return providers.toSorted((left, right) => {
const leftOrder = left.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
const rightOrder = right.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return comparePluginProvidersAlphabetically(left, right);
});
}
function pluginManifestDeclaresProviderConfig(
record: PluginManifestRecord,
configKey: WebProviderConfigKey,
contract: WebProviderContract,
): boolean {
if ((record.contracts?.[contract]?.length ?? 0) > 0) {
return true;
}
const configUiHintKeys = Object.keys(record.configUiHints ?? {});
if (configUiHintKeys.some((key) => key === configKey || key.startsWith(`${configKey}.`))) {
return true;
}
const properties = record.configSchema?.properties;
return typeof properties === "object" && properties !== null && configKey in properties;
}
export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: {
contract: WebProviderContract;
configKey: WebProviderConfigKey;
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
onlyPluginIds?: readonly string[];
origin?: PluginManifestRecord["origin"];
}): string[] | undefined {
const contractIds = new Set(
resolveManifestContractPluginIds({
contract: params.contract,
origin: params.origin,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
onlyPluginIds: params.onlyPluginIds,
}),
);
const onlyPluginIdSet =
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
const ids = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter(
(plugin) =>
(!params.origin || plugin.origin === params.origin) &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
(contractIds.has(plugin.id) ||
pluginManifestDeclaresProviderConfig(plugin, params.configKey, params.contract)),
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
return ids.length > 0 ? ids : undefined;
}
function resolveBundledWebProviderCompatPluginIds(params: {
contract: WebProviderContract;
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
return resolveManifestContractPluginIds({
contract: params.contract,
origin: "bundled",
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
}
export function resolveBundledWebProviderResolutionConfig(params: {
contract: WebProviderContract;
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
bundledAllowlistCompat?: boolean;
}): {
config: PluginLoadOptions["config"];
normalized: NormalizedPluginsConfig;
activationSourceConfig?: PluginLoadOptions["config"];
autoEnabledReasons: Record<string, string[]>;
} {
const activation = resolveBundledPluginCompatibleActivationInputs({
rawConfig: params.config,
env: params.env,
workspaceDir: params.workspaceDir,
applyAutoEnable: true,
compatMode: {
allowlist: params.bundledAllowlistCompat,
enablement: "always",
vitest: true,
},
resolveCompatPluginIds: (compatParams) =>
resolveBundledWebProviderCompatPluginIds({
contract: params.contract,
...compatParams,
}),
});
return {
config: activation.config,
normalized: activation.normalized,
activationSourceConfig: activation.activationSourceConfig,
autoEnabledReasons: activation.autoEnabledReasons,
};
}
export function buildWebProviderSnapshotCacheKey(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
bundledAllowlistCompat?: boolean;
onlyPluginIds?: readonly string[];
origin?: PluginManifestRecord["origin"];
envKey: string;
}): string {
return JSON.stringify({
workspaceDir: params.workspaceDir ?? "",
bundledAllowlistCompat: params.bundledAllowlistCompat === true,
origin: params.origin ?? "",
onlyPluginIds: [...new Set(params.onlyPluginIds ?? [])].toSorted((left, right) =>
left.localeCompare(right),
),
env: params.envKey,
});
}
export function mapRegistryProviders<
TProvider extends { id: string },
TEntry extends { pluginId: string; provider: TProvider },
>(params: {
entries: readonly TEntry[];
onlyPluginIds?: readonly string[];
sortProviders: (
providers: Array<TProvider & { pluginId: string }>,
) => Array<TProvider & { pluginId: string }>;
}): Array<TProvider & { pluginId: string }> {
const onlyPluginIdSet =
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
return params.sortProviders(
params.entries
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
})),
);
}