mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
refactor: unify plugin metadata consumers
This commit is contained in:
@@ -13,12 +13,12 @@ import { resolveUsableCustomProviderApiKey } from "../agents/model-auth.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
|
||||
import {
|
||||
isActivatedManifestOwner,
|
||||
passesManifestOwnerBasePolicy,
|
||||
} from "../plugins/manifest-owner-policy.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { resolveProviderAuthEnvVarCandidates } from "../secrets/provider-env-vars.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
@@ -181,12 +181,11 @@ function resolveUsageCredentialProviderIds(params: {
|
||||
const providerIds = new Set(normalizeProviderIds([params.provider]));
|
||||
const providerIdSet = new Set(providerIds);
|
||||
try {
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.state.cfg,
|
||||
env: params.state.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const plugin of snapshot.plugins) {
|
||||
const pluginProviderIds = normalizeProviderIds(plugin.providers);
|
||||
if (!pluginProviderIds.some((providerId) => providerIdSet.has(providerId))) {
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
|
||||
import { normalizeMediaProviderId } from "./provider-id.js";
|
||||
import type { MediaUnderstandingProvider } from "./types.js";
|
||||
|
||||
@@ -9,19 +8,12 @@ export function buildMediaUnderstandingManifestMetadataRegistry(
|
||||
workspaceDir?: string,
|
||||
): Map<string, MediaUnderstandingProvider> {
|
||||
const registry = new Map<string, MediaUnderstandingProvider>();
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
});
|
||||
const plugins =
|
||||
snapshot?.plugins ??
|
||||
loadPluginManifestRegistryForPluginRegistry({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
}).plugins;
|
||||
for (const plugin of plugins) {
|
||||
for (const plugin of snapshot.plugins) {
|
||||
const declaredProviders = new Set(
|
||||
(plugin.contracts?.mediaUnderstandingProviders ?? []).map((providerId) =>
|
||||
normalizeMediaProviderId(providerId),
|
||||
|
||||
@@ -68,6 +68,9 @@ describe("channel inbound roots fast path", () => {
|
||||
ctx: createContext("localchat"),
|
||||
}),
|
||||
).toEqual(["/remote/work"]);
|
||||
expect(
|
||||
publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync,
|
||||
).toHaveBeenCalledOnce();
|
||||
expect(publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync).toHaveBeenCalledWith(
|
||||
{
|
||||
dirName: "localchat",
|
||||
@@ -108,4 +111,35 @@ describe("channel inbound roots fast path", () => {
|
||||
artifactBasename: "index.js",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves partial media contract modules when a missing resolver is checked first", () => {
|
||||
publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync.mockImplementation(
|
||||
({ artifactBasename, dirName }: { artifactBasename: string; dirName: string }) => {
|
||||
if (dirName === "partialchat" && artifactBasename === "media-contract-api.js") {
|
||||
return {
|
||||
resolveInboundAttachmentRoots: ({ accountId }: { accountId?: string }) => [
|
||||
`/partial/${accountId}`,
|
||||
],
|
||||
};
|
||||
}
|
||||
throw unableToResolve(dirName, artifactBasename);
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveChannelRemoteInboundAttachmentRoots({
|
||||
cfg,
|
||||
ctx: createContext("partialchat"),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveChannelInboundAttachmentRoots({
|
||||
cfg,
|
||||
ctx: createContext("partialchat"),
|
||||
}),
|
||||
).toEqual(["/partial/work"]);
|
||||
expect(
|
||||
publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync,
|
||||
).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,19 +15,15 @@ type ChannelMediaContractApi = {
|
||||
};
|
||||
type ChannelMediaRootResolver = keyof ChannelMediaContractApi;
|
||||
|
||||
const mediaContractApiByResolver = new Map<string, ChannelMediaContractApi | null>();
|
||||
|
||||
function mediaContractCacheKey(channelId: string, resolver: ChannelMediaRootResolver): string {
|
||||
return `${channelId}:${resolver}`;
|
||||
}
|
||||
const mediaContractApiByChannel = new Map<string, ChannelMediaContractApi | null>();
|
||||
|
||||
function loadChannelMediaContractApi(
|
||||
channelId: string,
|
||||
resolver: ChannelMediaRootResolver,
|
||||
): ChannelMediaContractApi | undefined {
|
||||
const cacheKey = mediaContractCacheKey(channelId, resolver);
|
||||
if (mediaContractApiByResolver.has(cacheKey)) {
|
||||
return mediaContractApiByResolver.get(cacheKey) ?? undefined;
|
||||
if (mediaContractApiByChannel.has(channelId)) {
|
||||
const cached = mediaContractApiByChannel.get(channelId);
|
||||
return cached && typeof cached[resolver] === "function" ? cached : undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -35,10 +31,11 @@ function loadChannelMediaContractApi(
|
||||
dirName: channelId,
|
||||
artifactBasename: "media-contract-api.js",
|
||||
});
|
||||
mediaContractApiByChannel.set(channelId, loaded);
|
||||
if (typeof loaded[resolver] === "function") {
|
||||
mediaContractApiByResolver.set(cacheKey, loaded);
|
||||
return loaded;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
if (
|
||||
!(
|
||||
@@ -50,7 +47,7 @@ function loadChannelMediaContractApi(
|
||||
}
|
||||
}
|
||||
|
||||
mediaContractApiByResolver.set(cacheKey, null);
|
||||
mediaContractApiByChannel.set(channelId, null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
92
src/plugins/bundled-manifest-contract-plugins.ts
Normal file
92
src/plugins/bundled-manifest-contract-plugins.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
resolveBundledPluginCompatibleLoadValues,
|
||||
type PluginActivationBundledCompatMode,
|
||||
} from "./activation-context.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
} from "./config-state.js";
|
||||
import { loadManifestContractSnapshot } from "./manifest-contract-eligibility.js";
|
||||
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
|
||||
|
||||
function createPluginIdSet(pluginIds: readonly string[] | undefined): Set<string> | null {
|
||||
return pluginIds && pluginIds.length > 0 ? new Set(pluginIds) : null;
|
||||
}
|
||||
|
||||
export function listBundledManifestContractPluginIds(params: {
|
||||
plugins: readonly PluginManifestRecord[];
|
||||
contract: PluginManifestContractListKey;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet = createPluginIdSet(params.onlyPluginIds);
|
||||
return params.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(plugin.contracts?.[params.contract]?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveEnabledBundledManifestContractPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
contract: PluginManifestContractListKey;
|
||||
compatMode: PluginActivationBundledCompatMode;
|
||||
}): PluginManifestRecord[] {
|
||||
if (params.config?.plugins?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
let manifestRecords: readonly PluginManifestRecord[] | undefined;
|
||||
const loadManifestRecords = (config?: OpenClawConfig) => {
|
||||
manifestRecords ??= loadManifestContractSnapshot({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins;
|
||||
return manifestRecords;
|
||||
};
|
||||
|
||||
const activation = resolveBundledPluginCompatibleLoadValues({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
applyAutoEnable: true,
|
||||
compatMode: params.compatMode,
|
||||
resolveCompatPluginIds: (compatParams) =>
|
||||
listBundledManifestContractPluginIds({
|
||||
plugins: loadManifestRecords(compatParams.config),
|
||||
contract: params.contract,
|
||||
onlyPluginIds: compatParams.onlyPluginIds,
|
||||
}),
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins);
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: activation.activationSourceConfig,
|
||||
});
|
||||
const onlyPluginIdSet = createPluginIdSet(params.onlyPluginIds);
|
||||
return loadManifestRecords(activation.config).filter((plugin) => {
|
||||
if (
|
||||
plugin.origin !== "bundled" ||
|
||||
(onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) ||
|
||||
(plugin.contracts?.[params.contract]?.length ?? 0) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedPlugins,
|
||||
rootConfig: activation.config,
|
||||
enabledByDefault: plugin.enabledByDefault,
|
||||
activationSource,
|
||||
}).enabled;
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolvePluginDocumentExtractors } from "./document-extractors.runtime.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||
|
||||
vi.mock("./document-extractor-public-artifacts.js", () => ({
|
||||
loadBundledDocumentExtractorEntriesFromDir: vi.fn(
|
||||
@@ -19,36 +19,8 @@ vi.mock("./document-extractor-public-artifacts.js", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry-installed.js", () => ({
|
||||
loadPluginManifestRegistryForInstalledIndex: vi.fn(() => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "document-extract",
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
cliBackends: [],
|
||||
providers: [],
|
||||
legacyPluginIds: [],
|
||||
contracts: { documentExtractors: ["pdf"] },
|
||||
},
|
||||
{
|
||||
id: "openai",
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
cliBackends: [],
|
||||
providers: ["openai", "openai-codex"],
|
||||
legacyPluginIds: [],
|
||||
contracts: {},
|
||||
},
|
||||
],
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
|
||||
loadPluginManifestRegistryForPluginRegistry: vi.fn(() => ({
|
||||
vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||
loadPluginMetadataSnapshot: vi.fn(() => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "document-extract",
|
||||
@@ -80,10 +52,10 @@ vi.mock("./manifest-registry.js", () => ({
|
||||
|
||||
describe("resolvePluginDocumentExtractors", () => {
|
||||
it("reuses one manifest registry pass for compat and enabled bundled extractors", () => {
|
||||
vi.mocked(loadPluginManifestRegistryForPluginRegistry).mockClear();
|
||||
vi.mocked(loadPluginMetadataSnapshot).mockClear();
|
||||
|
||||
expect(resolvePluginDocumentExtractors().map((extractor) => extractor.id)).toEqual(["pdf"]);
|
||||
expect(loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledOnce();
|
||||
expect(loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("respects global plugin disablement", () => {
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveBundledPluginCompatibleLoadValues } from "./activation-context.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
} from "./config-state.js";
|
||||
import { resolveEnabledBundledManifestContractPlugins } from "./bundled-manifest-contract-plugins.js";
|
||||
import { loadBundledDocumentExtractorEntriesFromDir } from "./document-extractor-public-artifacts.js";
|
||||
import type { PluginDocumentExtractorEntry } from "./document-extractor-types.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
|
||||
function compareExtractors(
|
||||
left: PluginDocumentExtractorEntry,
|
||||
@@ -22,97 +15,6 @@ function compareExtractors(
|
||||
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
|
||||
}
|
||||
|
||||
function listDocumentExtractorPluginIds(params: {
|
||||
plugins: readonly PluginManifestRecord[];
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return params.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(plugin.contracts?.documentExtractors?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function loadDocumentExtractorManifestRecords(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): readonly PluginManifestRecord[] {
|
||||
return loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
}).plugins;
|
||||
}
|
||||
|
||||
function resolveEnabledBundledDocumentExtractorPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginManifestRecord[] {
|
||||
if (params.config?.plugins?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
let manifestRecords: readonly PluginManifestRecord[] | undefined;
|
||||
const loadManifestRecords = (config?: OpenClawConfig) => {
|
||||
manifestRecords ??= loadDocumentExtractorManifestRecords({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return manifestRecords;
|
||||
};
|
||||
|
||||
const activation = resolveBundledPluginCompatibleLoadValues({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
applyAutoEnable: true,
|
||||
compatMode: {
|
||||
allowlist: false,
|
||||
enablement: "allowlist",
|
||||
vitest: true,
|
||||
},
|
||||
resolveCompatPluginIds: (compatParams) =>
|
||||
listDocumentExtractorPluginIds({
|
||||
plugins: loadManifestRecords(compatParams.config),
|
||||
onlyPluginIds: compatParams.onlyPluginIds,
|
||||
}),
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins);
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: activation.activationSourceConfig,
|
||||
});
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return loadManifestRecords(activation.config).filter((plugin) => {
|
||||
if (
|
||||
plugin.origin !== "bundled" ||
|
||||
(onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) ||
|
||||
(plugin.contracts?.documentExtractors?.length ?? 0) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedPlugins,
|
||||
rootConfig: activation.config,
|
||||
enabledByDefault: plugin.enabledByDefault,
|
||||
activationSource,
|
||||
}).enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveExplicitAllowedDocumentExtractorPluginIds(params: {
|
||||
config?: OpenClawConfig;
|
||||
onlyPluginIds?: readonly string[];
|
||||
@@ -151,11 +53,17 @@ export function resolvePluginDocumentExtractors(params?: {
|
||||
});
|
||||
const pluginIds =
|
||||
explicitAllowedPluginIds ??
|
||||
resolveEnabledBundledDocumentExtractorPlugins({
|
||||
resolveEnabledBundledManifestContractPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
onlyPluginIds: params?.onlyPluginIds,
|
||||
contract: "documentExtractors",
|
||||
compatMode: {
|
||||
allowlist: false,
|
||||
enablement: "allowlist",
|
||||
vitest: true,
|
||||
},
|
||||
}).map((plugin) => plugin.id);
|
||||
for (const pluginId of pluginIds) {
|
||||
let loaded: PluginDocumentExtractorEntry[] | null;
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
resolveConfiguredChannelPluginIds,
|
||||
} from "./channel-plugin-ids.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
||||
import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
|
||||
function collectConfiguredChannelIds(
|
||||
config: OpenClawConfig,
|
||||
@@ -63,14 +63,13 @@ function collectBundledChannelOwnerPluginIds(params: {
|
||||
: {}),
|
||||
}
|
||||
: params.env;
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.config,
|
||||
env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const pluginIds = new Set<string>();
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const plugin of snapshot.plugins) {
|
||||
if (plugin.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type PluginManifestCommandAliasRegistry,
|
||||
type PluginManifestCommandAliasRecord,
|
||||
} from "./manifest-command-aliases.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { loadManifestMetadataRegistry } from "./manifest-contract-eligibility.js";
|
||||
|
||||
export function resolveManifestCommandAliasOwner(params: {
|
||||
command: string | undefined;
|
||||
@@ -15,12 +15,11 @@ export function resolveManifestCommandAliasOwner(params: {
|
||||
}): PluginManifestCommandAliasRecord | undefined {
|
||||
const registry =
|
||||
params.registry ??
|
||||
loadPluginManifestRegistryForPluginRegistry({
|
||||
loadManifestMetadataRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
}).manifestRegistry;
|
||||
return resolveManifestCommandAliasOwnerInRegistry({
|
||||
command: params.command,
|
||||
registry,
|
||||
|
||||
@@ -33,9 +33,10 @@ describe("loadManifestContractSnapshot", () => {
|
||||
};
|
||||
mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(current);
|
||||
|
||||
expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toBe(
|
||||
current,
|
||||
);
|
||||
expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toEqual({
|
||||
index: current.index,
|
||||
plugins: current.plugins,
|
||||
});
|
||||
|
||||
expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PluginManifestContractListKey, PluginManifestRecord } from "./mani
|
||||
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||
import type {
|
||||
PluginMetadataManifestView,
|
||||
PluginMetadataRegistryView,
|
||||
PluginMetadataSnapshot,
|
||||
} from "./plugin-metadata-snapshot.types.js";
|
||||
|
||||
@@ -68,6 +69,30 @@ export function loadManifestContractSnapshot(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): PluginMetadataManifestView {
|
||||
const snapshot = loadManifestMetadataSnapshot(params);
|
||||
return {
|
||||
index: snapshot.index,
|
||||
plugins: snapshot.plugins,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadManifestMetadataRegistry(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): PluginMetadataRegistryView {
|
||||
const snapshot = loadManifestMetadataSnapshot(params);
|
||||
return {
|
||||
index: snapshot.index,
|
||||
manifestRegistry: snapshot.manifestRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadManifestMetadataSnapshot(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): PluginMetadataSnapshot {
|
||||
const env = params.env ?? process.env;
|
||||
const current = getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
@@ -77,13 +102,9 @@ export function loadManifestContractSnapshot(params: {
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
const snapshot = loadPluginMetadataSnapshot({
|
||||
return loadPluginMetadataSnapshot({
|
||||
config: params.config ?? {},
|
||||
env,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
return {
|
||||
index: snapshot.index,
|
||||
plugins: snapshot.plugins,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: vi.fn(),
|
||||
loadPluginMetadataSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry,
|
||||
vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -13,12 +13,19 @@ import {
|
||||
resolveManifestBuiltInModelSuppression,
|
||||
} from "./manifest-model-suppression.js";
|
||||
|
||||
function createMetadataSnapshot(plugins: Record<string, unknown>[]) {
|
||||
return {
|
||||
index: { plugins: [] },
|
||||
diagnostics: [],
|
||||
plugins: plugins.map((plugin) => ({ origin: "bundled", ...plugin })),
|
||||
};
|
||||
}
|
||||
|
||||
describe("manifest model suppression", () => {
|
||||
beforeEach(() => {
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReset();
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
mocks.loadPluginMetadataSnapshot.mockReset();
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue(
|
||||
createMetadataSnapshot([
|
||||
{
|
||||
id: "openai",
|
||||
providers: ["openai"],
|
||||
@@ -41,8 +48,8 @@ describe("manifest model suppression", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
describe("buildManifestBuiltInModelSuppressionResolver", () => {
|
||||
@@ -54,7 +61,7 @@ describe("manifest model suppression", () => {
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolver({
|
||||
provider: "azure-openai-responses",
|
||||
@@ -65,7 +72,7 @@ describe("manifest model suppression", () => {
|
||||
id: "gpt-5.3-codex-spark",
|
||||
});
|
||||
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,7 +116,7 @@ describe("manifest model suppression", () => {
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses planned manifest suppressions inside a resolver instance", () => {
|
||||
@@ -132,13 +139,12 @@ describe("manifest model suppression", () => {
|
||||
id: "gpt-4.1",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("matches conditional suppressions by base URL host", () => {
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue(
|
||||
createMetadataSnapshot([
|
||||
{
|
||||
id: "qwen",
|
||||
providers: ["qwen", "modelstudio"],
|
||||
@@ -159,8 +165,8 @@ describe("manifest model suppression", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveManifestBuiltInModelSuppression({
|
||||
@@ -189,9 +195,8 @@ describe("manifest model suppression", () => {
|
||||
});
|
||||
|
||||
it("does not apply conditional suppressions to custom providers with a foreign api owner", () => {
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue(
|
||||
createMetadataSnapshot([
|
||||
{
|
||||
id: "qwen",
|
||||
providers: ["modelstudio"],
|
||||
@@ -208,8 +213,8 @@ describe("manifest model suppression", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveManifestBuiltInModelSuppression({
|
||||
|
||||
@@ -5,18 +5,31 @@ import {
|
||||
type ManifestModelCatalogSuppressionEntry,
|
||||
} from "../model-catalog/index.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import {
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestMetadataSnapshot,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
|
||||
function listManifestModelCatalogSuppressions(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): readonly ManifestModelCatalogSuppressionEntry[] {
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const registry = {
|
||||
diagnostics: snapshot.diagnostics,
|
||||
plugins: snapshot.plugins.filter((plugin) =>
|
||||
isManifestPluginAvailableForControlPlane({
|
||||
snapshot,
|
||||
plugin,
|
||||
config: params.config,
|
||||
}),
|
||||
),
|
||||
};
|
||||
const planned = planManifestModelCatalogSuppressions({ registry });
|
||||
return planned.suppressions;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveBundledPluginCompatibleLoadValues } from "./activation-context.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
} from "./config-state.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { resolveEnabledBundledManifestContractPlugins } from "./bundled-manifest-contract-plugins.js";
|
||||
import { loadBundledWebContentExtractorEntriesFromDir } from "./web-content-extractor-public-artifacts.js";
|
||||
import type { PluginWebContentExtractorEntry } from "./web-content-extractor-types.js";
|
||||
|
||||
@@ -22,97 +15,6 @@ function compareExtractors(
|
||||
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
|
||||
}
|
||||
|
||||
function listWebContentExtractorPluginIds(params: {
|
||||
plugins: readonly PluginManifestRecord[];
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return params.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(plugin.contracts?.webContentExtractors?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function loadWebContentExtractorManifestRecords(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): readonly PluginManifestRecord[] {
|
||||
return loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
}).plugins;
|
||||
}
|
||||
|
||||
function resolveEnabledBundledExtractorPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginManifestRecord[] {
|
||||
if (params.config?.plugins?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
let manifestRecords: readonly PluginManifestRecord[] | undefined;
|
||||
const loadManifestRecords = (config?: OpenClawConfig) => {
|
||||
manifestRecords ??= loadWebContentExtractorManifestRecords({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return manifestRecords;
|
||||
};
|
||||
|
||||
const activation = resolveBundledPluginCompatibleLoadValues({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
applyAutoEnable: true,
|
||||
compatMode: {
|
||||
allowlist: true,
|
||||
enablement: "always",
|
||||
vitest: true,
|
||||
},
|
||||
resolveCompatPluginIds: (compatParams) =>
|
||||
listWebContentExtractorPluginIds({
|
||||
plugins: loadManifestRecords(compatParams.config),
|
||||
onlyPluginIds: compatParams.onlyPluginIds,
|
||||
}),
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins);
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: activation.activationSourceConfig,
|
||||
});
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return loadManifestRecords(activation.config).filter((plugin) => {
|
||||
if (
|
||||
plugin.origin !== "bundled" ||
|
||||
(onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) ||
|
||||
(plugin.contracts?.webContentExtractors?.length ?? 0) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedPlugins,
|
||||
rootConfig: activation.config,
|
||||
enabledByDefault: plugin.enabledByDefault,
|
||||
activationSource,
|
||||
}).enabled;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginWebContentExtractors(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -120,11 +22,17 @@ export function resolvePluginWebContentExtractors(params?: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebContentExtractorEntry[] {
|
||||
const extractors: PluginWebContentExtractorEntry[] = [];
|
||||
for (const plugin of resolveEnabledBundledExtractorPlugins({
|
||||
for (const plugin of resolveEnabledBundledManifestContractPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
onlyPluginIds: params?.onlyPluginIds,
|
||||
contract: "webContentExtractors",
|
||||
compatMode: {
|
||||
allowlist: true,
|
||||
enablement: "always",
|
||||
vitest: true,
|
||||
},
|
||||
})) {
|
||||
const loaded = loadBundledWebContentExtractorEntriesFromDir({
|
||||
dirName: plugin.id,
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: vi.fn(),
|
||||
loadPluginMetadataSnapshot: vi.fn(),
|
||||
loadPluginRegistrySnapshotWithMetadata: vi.fn(),
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts: vi.fn(() => null),
|
||||
resolveBundledExplicitWebFetchProvidersFromPublicArtifacts: vi.fn(() => null),
|
||||
loadBundledWebSearchProviderEntriesFromDir: vi.fn(),
|
||||
loadBundledWebFetchProviderEntriesFromDir: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry,
|
||||
loadPluginRegistrySnapshotWithMetadata: mocks.loadPluginRegistrySnapshotWithMetadata,
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
|
||||
}));
|
||||
@@ -48,23 +41,6 @@ const {
|
||||
describe("web provider public artifact manifest fallback", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "fallback-search",
|
||||
origin: "bundled",
|
||||
rootDir: "/tmp/fallback-search",
|
||||
contracts: { webSearchProviders: ["fallback-search"] },
|
||||
},
|
||||
{
|
||||
id: "fallback-fetch",
|
||||
origin: "bundled",
|
||||
rootDir: "/tmp/fallback-fetch",
|
||||
contracts: { webFetchProviders: ["fallback-fetch"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
@@ -82,11 +58,6 @@ describe("web provider public artifact manifest fallback", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "derived",
|
||||
snapshot: { plugins: [] },
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.loadBundledWebSearchProviderEntriesFromDir.mockReturnValue([
|
||||
{ id: "fallback-search", pluginId: "fallback-search" },
|
||||
]);
|
||||
@@ -100,7 +71,6 @@ describe("web provider public artifact manifest fallback", () => {
|
||||
|
||||
expect(providers).toEqual([{ id: "fallback-search", pluginId: "fallback-search" }]);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.loadBundledWebSearchProviderEntriesFromDir).toHaveBeenCalledWith({
|
||||
dirName: "fallback-search",
|
||||
pluginId: "fallback-search",
|
||||
@@ -112,7 +82,6 @@ describe("web provider public artifact manifest fallback", () => {
|
||||
|
||||
expect(providers).toEqual([{ id: "fallback-fetch", pluginId: "fallback-fetch" }]);
|
||||
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.loadBundledWebFetchProviderEntriesFromDir).toHaveBeenCalledWith({
|
||||
dirName: "fallback-fetch",
|
||||
pluginId: "fallback-fetch",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import type { PluginWebFetchProviderEntry, PluginWebSearchProviderEntry } from "./types.js";
|
||||
import { resolveBundledWebFetchResolutionConfig } from "./web-fetch-providers.shared.js";
|
||||
import {
|
||||
@@ -71,11 +71,10 @@ function resolveBundledManifestRecordsByPluginId(params: {
|
||||
const allowedPluginIds = new Set(params.onlyPluginIds);
|
||||
const manifestRecords =
|
||||
params.manifestRecords ??
|
||||
loadPluginManifestRegistryForPluginRegistry({
|
||||
loadManifestMetadataSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
}).plugins;
|
||||
return new Map(
|
||||
manifestRecords
|
||||
|
||||
Reference in New Issue
Block a user