mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 04:57:48 +00:00
* perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param). * perf(plugins): drop verbose JSDoc from discovery? params
226 lines
7.0 KiB
TypeScript
226 lines
7.0 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { isRecord } from "../utils.js";
|
|
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
|
|
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
|
import type { PluginManifestConfigContracts } from "./manifest.js";
|
|
import type { PluginOrigin } from "./plugin-origin.types.js";
|
|
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
|
|
|
export type PluginConfigContractMatch = {
|
|
path: string;
|
|
value: unknown;
|
|
};
|
|
|
|
export type PluginConfigContractMetadata = {
|
|
origin: PluginOrigin;
|
|
configContracts: PluginManifestConfigContracts;
|
|
};
|
|
|
|
type TraversalState = {
|
|
segments: string[];
|
|
value: unknown;
|
|
};
|
|
|
|
function normalizePathPattern(pathPattern: string): string[] {
|
|
return pathPattern
|
|
.split(".")
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function appendPathSegment(path: string, segment: string): string {
|
|
if (!path) {
|
|
return segment;
|
|
}
|
|
return /^\d+$/.test(segment) ? `${path}[${segment}]` : `${path}.${segment}`;
|
|
}
|
|
|
|
function parseCanonicalArrayIndex(segment: string, length: number): number | null {
|
|
if (!/^(0|[1-9]\d*)$/.test(segment)) {
|
|
return null;
|
|
}
|
|
const index = Number(segment);
|
|
return Number.isSafeInteger(index) && index >= 0 && index < length ? index : null;
|
|
}
|
|
|
|
export function collectPluginConfigContractMatches(params: {
|
|
root: unknown;
|
|
pathPattern: string;
|
|
}): PluginConfigContractMatch[] {
|
|
const pattern = normalizePathPattern(params.pathPattern);
|
|
if (pattern.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
let states: TraversalState[] = [{ segments: [], value: params.root }];
|
|
for (const segment of pattern) {
|
|
const nextStates: TraversalState[] = [];
|
|
for (const state of states) {
|
|
if (segment === "*") {
|
|
if (Array.isArray(state.value)) {
|
|
for (const [index, value] of state.value.entries()) {
|
|
nextStates.push({
|
|
segments: [...state.segments, String(index)],
|
|
value,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (isRecord(state.value)) {
|
|
for (const [key, value] of Object.entries(state.value)) {
|
|
nextStates.push({
|
|
segments: [...state.segments, key],
|
|
value,
|
|
});
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (Array.isArray(state.value)) {
|
|
const index = parseCanonicalArrayIndex(segment, state.value.length);
|
|
if (index !== null) {
|
|
nextStates.push({
|
|
segments: [...state.segments, segment],
|
|
value: state.value[index],
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (!isRecord(state.value) || !Object.prototype.hasOwnProperty.call(state.value, segment)) {
|
|
continue;
|
|
}
|
|
nextStates.push({
|
|
segments: [...state.segments, segment],
|
|
value: state.value[segment],
|
|
});
|
|
}
|
|
states = nextStates;
|
|
if (states.length === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return states.map((state) => ({
|
|
path: state.segments.reduce(appendPathSegment, ""),
|
|
value: state.value,
|
|
}));
|
|
}
|
|
|
|
export function resolvePluginConfigContractsById(params: {
|
|
config?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
fallbackToBundledMetadata?: boolean;
|
|
fallbackToBundledMetadataForResolvedBundled?: boolean;
|
|
fallbackBundledPluginIds?: readonly string[];
|
|
pluginIds: readonly string[];
|
|
discovery?: PluginDiscoveryResult;
|
|
}): ReadonlyMap<string, PluginConfigContractMetadata> {
|
|
const matches = new Map<string, PluginConfigContractMetadata>();
|
|
const pluginIds = [
|
|
...new Set(params.pluginIds.map((pluginId) => pluginId.trim()).filter(Boolean)),
|
|
];
|
|
if (pluginIds.length === 0) {
|
|
return matches;
|
|
}
|
|
const fallbackBundledPluginIds = new Set(
|
|
(params.fallbackBundledPluginIds ?? []).map((pluginId) => pluginId.trim()).filter(Boolean),
|
|
);
|
|
const bundledContractFallbacks = new Map<string, PluginManifestConfigContracts | undefined>();
|
|
const findBundledConfigContracts = (
|
|
pluginId: string,
|
|
): PluginManifestConfigContracts | undefined => {
|
|
if (bundledContractFallbacks.has(pluginId)) {
|
|
return bundledContractFallbacks.get(pluginId);
|
|
}
|
|
const discovery =
|
|
params.discovery ??
|
|
discoverOpenClawPlugins({
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
});
|
|
const registry = loadPluginManifestRegistry({
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
candidates: discovery.candidates.filter((candidate) => candidate.origin === "bundled"),
|
|
diagnostics: discovery.diagnostics,
|
|
});
|
|
for (const plugin of registry.plugins) {
|
|
bundledContractFallbacks.set(plugin.id, plugin.configContracts);
|
|
}
|
|
if (!bundledContractFallbacks.has(pluginId)) {
|
|
bundledContractFallbacks.set(pluginId, undefined);
|
|
}
|
|
return bundledContractFallbacks.get(pluginId);
|
|
};
|
|
|
|
const resolvedPluginOrigins = new Map<string, PluginOrigin>();
|
|
const registry = loadPluginManifestRegistryForPluginRegistry({
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
includeDisabled: true,
|
|
});
|
|
for (const plugin of registry.plugins) {
|
|
if (!pluginIds.includes(plugin.id)) {
|
|
continue;
|
|
}
|
|
resolvedPluginOrigins.set(plugin.id, plugin.origin);
|
|
if (!plugin.configContracts) {
|
|
continue;
|
|
}
|
|
matches.set(plugin.id, {
|
|
origin: plugin.origin,
|
|
configContracts: plugin.configContracts,
|
|
});
|
|
}
|
|
|
|
if (params.fallbackToBundledMetadata ?? true) {
|
|
for (const pluginId of pluginIds) {
|
|
const existing = matches.get(pluginId);
|
|
const shouldHydrateBundledMatch =
|
|
existing &&
|
|
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
|
|
fallbackBundledPluginIds.has(pluginId));
|
|
if (shouldHydrateBundledMatch) {
|
|
const bundledConfigContracts = findBundledConfigContracts(pluginId);
|
|
if (bundledConfigContracts) {
|
|
matches.set(pluginId, {
|
|
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
|
|
configContracts: {
|
|
...bundledConfigContracts,
|
|
...existing.configContracts,
|
|
...(bundledConfigContracts.secretInputs
|
|
? { secretInputs: bundledConfigContracts.secretInputs }
|
|
: {}),
|
|
},
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (matches.has(pluginId)) {
|
|
continue;
|
|
}
|
|
const resolvedOrigin = resolvedPluginOrigins.get(pluginId);
|
|
if (
|
|
resolvedOrigin &&
|
|
!(params.fallbackToBundledMetadataForResolvedBundled && resolvedOrigin === "bundled") &&
|
|
!fallbackBundledPluginIds.has(pluginId)
|
|
) {
|
|
continue;
|
|
}
|
|
const bundledConfigContracts = findBundledConfigContracts(pluginId);
|
|
if (!bundledConfigContracts) {
|
|
continue;
|
|
}
|
|
matches.set(pluginId, {
|
|
origin: "bundled",
|
|
configContracts: bundledConfigContracts,
|
|
});
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|