mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix: stop repeated static catalog manifest scans (#125090)
This commit is contained in:
committed by
GitHub
parent
35695cb3bc
commit
f77d2ec3a8
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
|
||||
const manifestMocks = vi.hoisted(() => ({
|
||||
@@ -108,6 +109,7 @@ function setManifestPlugins(plugins: unknown[]) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
for (const mock of Object.values(manifestMocks)) {
|
||||
mock.mockReset();
|
||||
}
|
||||
@@ -284,6 +286,56 @@ describe("bundled static model catalog snapshot cache", () => {
|
||||
expect(manifestMocks.loadPluginManifest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("refreshes the no-snapshot memo only at the plugin metadata lifecycle boundary", () => {
|
||||
const env = { HOME: "/custom-home" };
|
||||
const plugin = createMistralManifestPlugin();
|
||||
setManifestPlugins([
|
||||
{ ...plugin, modelCatalog: { ...plugin.modelCatalog, runtimeAugment: true } },
|
||||
]);
|
||||
|
||||
expect(bundledStaticCatalogProviderUsesRuntimeAugment({ provider: "mistral", env })).toBe(true);
|
||||
expect(bundledStaticCatalogProviderUsesRuntimeAugment({ provider: "mistral", env })).toBe(true);
|
||||
expect(manifestMocks.listOpenClawPluginManifestMetadata).toHaveBeenCalledTimes(1);
|
||||
|
||||
setManifestPlugins([
|
||||
{ ...plugin, modelCatalog: { ...plugin.modelCatalog, runtimeAugment: false } },
|
||||
]);
|
||||
expect(bundledStaticCatalogProviderUsesRuntimeAugment({ provider: "mistral", env })).toBe(true);
|
||||
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
expect(bundledStaticCatalogProviderUsesRuntimeAugment({ provider: "mistral", env })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(manifestMocks.listOpenClawPluginManifestMetadata).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("refreshes a retained no-snapshot resolver at the plugin metadata lifecycle boundary", () => {
|
||||
const env = { HOME: "/custom-home" };
|
||||
const firstPlugin = createMistralManifestPlugin();
|
||||
setManifestPlugins([firstPlugin]);
|
||||
const resolveModel = createBundledStaticCatalogModelResolver({ env });
|
||||
|
||||
expect(resolveModel({ provider: "mistral", modelId: "mistral-medium-3-5" })?.id).toBe(
|
||||
"mistral-medium-3-5",
|
||||
);
|
||||
|
||||
const replacementPlugin = createMistralManifestPlugin();
|
||||
replacementPlugin.modelCatalog.providers.mistral.models =
|
||||
replacementPlugin.modelCatalog.providers.mistral.models.map((model) => ({
|
||||
...model,
|
||||
id: "mistral-medium-next",
|
||||
name: "Mistral Medium Next",
|
||||
}));
|
||||
setManifestPlugins([replacementPlugin]);
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
|
||||
expect(resolveModel({ provider: "mistral", modelId: "mistral-medium-3-5" })).toBeUndefined();
|
||||
expect(resolveModel({ provider: "mistral", modelId: "mistral-medium-next" })?.name).toBe(
|
||||
"Mistral Medium Next",
|
||||
);
|
||||
expect(manifestMocks.listOpenClawPluginManifestMetadata).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("preserves plugin enablement policy for current snapshot catalog rows", () => {
|
||||
setCurrentManifestPlugins([createMistralManifestPlugin()]);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ vi.mock("../../plugins/provider-discovery.js", async (importOriginal) => ({
|
||||
runProviderStaticCatalog: providerMocks.runProviderStaticCatalog,
|
||||
}));
|
||||
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import { getModelProviderRequestTransport } from "../provider-request-config.js";
|
||||
import {
|
||||
createBundledProviderStaticCatalogContextResolver,
|
||||
@@ -192,6 +193,7 @@ function expectManifestAliasResolution(
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
manifestMocks.getCurrentPluginMetadataSnapshot.mockReset();
|
||||
manifestMocks.listOpenClawPluginManifestMetadata.mockReset();
|
||||
manifestMocks.loadPluginManifest.mockReset();
|
||||
|
||||
@@ -12,6 +12,7 @@ import { listOpenClawPluginManifestMetadata } from "../../plugins/manifest-metad
|
||||
import { passesManifestOwnerBasePolicy } from "../../plugins/manifest-owner-policy.js";
|
||||
import { loadPluginManifestRegistryCore } from "../../plugins/manifest-registry.js";
|
||||
import { loadPluginManifest } from "../../plugins/manifest.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
normalizePluginDiscoveryResult,
|
||||
@@ -145,14 +146,20 @@ type BundledStaticCatalogState = {
|
||||
plans: Map<string, ReturnType<typeof planEffectiveModelCatalogRows>>;
|
||||
};
|
||||
|
||||
// Snapshot identity changes at the Gateway reload commit, so old provider plans
|
||||
// cannot survive into a replacement plugin generation.
|
||||
const bundledStaticCatalogStatesBySnapshot = new WeakMap<
|
||||
PluginMetadataSnapshot,
|
||||
let bundledStaticCatalogStatesByOwner = new WeakMap<
|
||||
object,
|
||||
WeakMap<OpenClawConfig, BundledStaticCatalogState>
|
||||
>();
|
||||
const defaultBundledStaticCatalogConfig: OpenClawConfig = {};
|
||||
|
||||
function clearBundledStaticCatalogStates(): void {
|
||||
bundledStaticCatalogStatesByOwner = new WeakMap();
|
||||
}
|
||||
|
||||
// Snapshot or environment identity pins one plugin generation; install/reload
|
||||
// owners replace this map so retained resolvers cannot keep stale provider plans.
|
||||
registerPluginMetadataProcessMemoLifecycleClear(clearBundledStaticCatalogStates);
|
||||
|
||||
function resolveBundledStaticCatalogMetadataSnapshot(
|
||||
params: BundledStaticCatalogParams,
|
||||
): PluginMetadataSnapshot | undefined {
|
||||
@@ -203,14 +210,15 @@ function listBundledStaticCatalogPlugins(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSnapshotBundledStaticCatalogState(
|
||||
function resolveBundledStaticCatalogState(
|
||||
params: BundledStaticCatalogParams,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
metadataSnapshot?: PluginMetadataSnapshot,
|
||||
): BundledStaticCatalogState {
|
||||
let states = bundledStaticCatalogStatesBySnapshot.get(metadataSnapshot);
|
||||
const owner = metadataSnapshot ?? params.env;
|
||||
let states = bundledStaticCatalogStatesByOwner.get(owner);
|
||||
if (!states) {
|
||||
states = new WeakMap();
|
||||
bundledStaticCatalogStatesBySnapshot.set(metadataSnapshot, states);
|
||||
bundledStaticCatalogStatesByOwner.set(owner, states);
|
||||
}
|
||||
const config = params.cfg ?? defaultBundledStaticCatalogConfig;
|
||||
const cached = states.get(config);
|
||||
@@ -230,6 +238,7 @@ export function bundledStaticCatalogProviderUsesRuntimeAugment(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
}): boolean {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
@@ -239,12 +248,11 @@ export function bundledStaticCatalogProviderUsesRuntimeAugment(params: {
|
||||
const catalogParams = {
|
||||
cfg: params.cfg,
|
||||
env: params.env ?? process.env,
|
||||
...(params.metadataSnapshot ? { metadataSnapshot: params.metadataSnapshot } : {}),
|
||||
workspaceDir: params.workspaceDir,
|
||||
};
|
||||
const metadataSnapshot = resolveBundledStaticCatalogMetadataSnapshot(catalogParams);
|
||||
const plugins = metadataSnapshot
|
||||
? resolveSnapshotBundledStaticCatalogState(catalogParams, metadataSnapshot).plugins
|
||||
: listBundledStaticCatalogPlugins(catalogParams);
|
||||
const plugins = resolveBundledStaticCatalogState(catalogParams, metadataSnapshot).plugins;
|
||||
return plugins.some((plugin) => {
|
||||
const catalog = plugin.modelCatalog;
|
||||
if (catalog?.runtimeAugment !== true) {
|
||||
@@ -305,19 +313,13 @@ export function createBundledStaticCatalogModelResolver(params?: {
|
||||
const matchesStaticModelId = params?.metadataSnapshot
|
||||
? createStaticModelIdMatcher({ manifestPlugins: params.metadataSnapshot.plugins })
|
||||
: staticModelIdMatches;
|
||||
let standaloneState: BundledStaticCatalogState | undefined;
|
||||
return (lookup) => {
|
||||
const provider = normalizeProviderId(lookup.provider);
|
||||
if (!provider || !lookup.modelId.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const metadataSnapshot = resolveBundledStaticCatalogMetadataSnapshot(catalogParams);
|
||||
const state = metadataSnapshot
|
||||
? resolveSnapshotBundledStaticCatalogState(catalogParams, metadataSnapshot)
|
||||
: (standaloneState ??= {
|
||||
plugins: listBundledStaticCatalogPlugins(catalogParams),
|
||||
plans: new Map(),
|
||||
});
|
||||
const state = resolveBundledStaticCatalogState(catalogParams, metadataSnapshot);
|
||||
if (state.plugins.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
describeImagesWithModel,
|
||||
MediaUnderstandingProvider,
|
||||
} from "../../plugin-sdk/media-understanding.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { AuthProfileStore } from "../auth-profiles/types.js";
|
||||
import type { resolveBundledStaticCatalogModel } from "../embedded-agent-runner/model.static-catalog.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js";
|
||||
@@ -51,6 +52,7 @@ type ResolveImageCompressionPolicy = (params: {
|
||||
imageCount: number;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}) => Promise<ImageCompressionPolicy>;
|
||||
|
||||
type ImageToolProviderDeps = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isInboundPathAllowed } from "@openclaw/media-core/inbound-path-policy";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createPluginMetadataSnapshot } from "../../config/plugin-auto-enable.test-helpers.js";
|
||||
import type { ModelDefinitionConfig } from "../../config/types.models.js";
|
||||
import { encodePngRgba, fillPixel } from "../../media/png-encode.js";
|
||||
import type {
|
||||
@@ -15,6 +16,8 @@ import type {
|
||||
ImagesDescriptionRequest,
|
||||
MediaUnderstandingProvider,
|
||||
} from "../../plugin-sdk/media-understanding.js";
|
||||
import { installTemporaryCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
|
||||
import { withEnvAsync } from "../../test-utils/env.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "../auth-profiles/types.js";
|
||||
@@ -3149,6 +3152,103 @@ describe("image compression policy", () => {
|
||||
testing.setProviderDepsForTest();
|
||||
});
|
||||
|
||||
it("keeps runtime augmentation pinned to the prepared plugin generation", async () => {
|
||||
const provider = "prepared-image-provider";
|
||||
const model = "prepared-image-model";
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
[provider]: {
|
||||
baseUrl: "https://prepared-image.example.test/v1",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: model,
|
||||
name: "Prepared image model",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 8_192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const createSnapshot = (runtimeAugment: boolean) => {
|
||||
const plugin = {
|
||||
id: "prepared-image-plugin",
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
providers: [provider],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
origin: "bundled",
|
||||
rootDir: "/fake/prepared-image-plugin",
|
||||
source: "/fake/prepared-image-plugin/index.js",
|
||||
manifestPath: "/fake/prepared-image-plugin/openclaw.plugin.json",
|
||||
modelCatalog: {
|
||||
runtimeAugment,
|
||||
providers: {
|
||||
[provider]: {
|
||||
baseUrl: "https://prepared-image.example.test/v1",
|
||||
api: "openai-completions",
|
||||
models: [{ id: model, name: "Prepared image model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies PluginManifestRecord;
|
||||
return createPluginMetadataSnapshot({
|
||||
config: cfg,
|
||||
manifestRegistry: { plugins: [plugin], diagnostics: [] },
|
||||
});
|
||||
};
|
||||
const preparedSnapshot = createSnapshot(false);
|
||||
const currentSnapshot = createSnapshot(true);
|
||||
const hookModes: Array<boolean | undefined> = [];
|
||||
installImageUnderstandingProviderDeps([], {
|
||||
resolveModelAsync: async (resolvedProvider, resolvedModel, _agentDir, _cfg, options) => {
|
||||
hookModes.push(options?.skipProviderRuntimeHooks);
|
||||
return {
|
||||
model: {
|
||||
id: resolvedModel,
|
||||
provider: resolvedProvider,
|
||||
input: ["text", "image"],
|
||||
mediaInput: {
|
||||
image: options?.skipProviderRuntimeHooks
|
||||
? { maxBytes: 1_000_000 }
|
||||
: { maxSidePx: 4096 },
|
||||
},
|
||||
} as never,
|
||||
authStorage: {} as never,
|
||||
modelRegistry: {} as never,
|
||||
};
|
||||
},
|
||||
});
|
||||
const currentLease = installTemporaryCurrentPluginMetadataSnapshot(currentSnapshot, {
|
||||
config: cfg,
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(
|
||||
testing.resolveImageCompressionPolicy({
|
||||
cfg,
|
||||
imageModelConfig: { primary: `${provider}/${model}` },
|
||||
imageCount: 1,
|
||||
metadataSnapshot: preparedSnapshot,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
imageCount: 1,
|
||||
models: [{ maxBytes: 1_000_000 }],
|
||||
});
|
||||
expect(hookModes).toEqual([true]);
|
||||
} finally {
|
||||
currentLease.release();
|
||||
}
|
||||
});
|
||||
|
||||
it("derives model metadata, quality preference, and image count from config", async () => {
|
||||
const cfg = {
|
||||
...cfgWithImageModelMetadata,
|
||||
|
||||
@@ -496,9 +496,6 @@ function providerUsesRuntimeModelAugment(params: {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
if (bundledStaticCatalogProviderUsesRuntimeAugment({ provider })) {
|
||||
return true;
|
||||
}
|
||||
const config = params.cfg ?? {};
|
||||
const preparedSnapshot =
|
||||
params.metadataSnapshot &&
|
||||
@@ -518,6 +515,16 @@ function providerUsesRuntimeModelAugment(params: {
|
||||
env: process.env,
|
||||
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
if (
|
||||
bundledStaticCatalogProviderUsesRuntimeAugment({
|
||||
provider,
|
||||
cfg: params.cfg,
|
||||
...(snapshot ? { metadataSnapshot: snapshot } : {}),
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!snapshot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { writePersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
|
||||
import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
@@ -22,11 +23,60 @@ function writeJson(filePath: string, value: unknown): void {
|
||||
|
||||
describe("listOpenClawPluginManifestMetadata", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps manifest metadata stable until explicit lifecycle invalidation", () => {
|
||||
const root = createTempRoot();
|
||||
const home = path.join(root, "home");
|
||||
const bundledRoot = path.join(root, "extensions");
|
||||
const pluginDir = path.join(bundledRoot, "lifecycle-catalog");
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
const env = {
|
||||
HOME: home,
|
||||
OPENCLAW_HOME: home,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
|
||||
};
|
||||
const writeManifest = (generation: string) =>
|
||||
writeJson(manifestPath, { id: "lifecycle-catalog", generation });
|
||||
|
||||
writeManifest("first");
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
const statSpy = vi.spyOn(fs, "statSync");
|
||||
const readdirSpy = vi.spyOn(fs, "readdirSync");
|
||||
|
||||
expect(
|
||||
listOpenClawPluginManifestMetadata(env).find(
|
||||
(record) => record.manifest.id === "lifecycle-catalog",
|
||||
)?.manifest.generation,
|
||||
).toBe("first");
|
||||
const firstStatCalls = statSpy.mock.calls.length;
|
||||
const firstReaddirCalls = readdirSpy.mock.calls.length;
|
||||
expect(firstReaddirCalls).toBeGreaterThan(0);
|
||||
|
||||
writeManifest("second");
|
||||
expect(
|
||||
listOpenClawPluginManifestMetadata(env).find(
|
||||
(record) => record.manifest.id === "lifecycle-catalog",
|
||||
)?.manifest.generation,
|
||||
).toBe("first");
|
||||
expect(statSpy).toHaveBeenCalledTimes(firstStatCalls);
|
||||
expect(readdirSpy).toHaveBeenCalledTimes(firstReaddirCalls);
|
||||
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
expect(
|
||||
listOpenClawPluginManifestMetadata(env).find(
|
||||
(record) => record.manifest.id === "lifecycle-catalog",
|
||||
)?.manifest.generation,
|
||||
).toBe("second");
|
||||
expect(statSpy).toHaveBeenCalledTimes(firstStatCalls);
|
||||
expect(readdirSpy.mock.calls.length).toBeGreaterThan(firstReaddirCalls);
|
||||
});
|
||||
|
||||
it("prefers the active bundled manifest over stale persisted bundled installs", () => {
|
||||
const root = createTempRoot();
|
||||
const home = path.join(root, "home");
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { resolveDefaultPluginExtensionsDir } from "./install-paths.js";
|
||||
import { readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
|
||||
|
||||
// Plugin manifest files are small metadata descriptors. Bound reads to prevent
|
||||
// a corrupted or hostile manifest from exhausting memory during metadata scan.
|
||||
@@ -33,12 +34,15 @@ type CandidateDir = {
|
||||
};
|
||||
|
||||
const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
||||
let manifestMetadataCache:
|
||||
| {
|
||||
key: string;
|
||||
records: PluginManifestMetadataRecord[];
|
||||
}
|
||||
| undefined;
|
||||
let manifestMetadataCache = new WeakMap<NodeJS.ProcessEnv, PluginManifestMetadataRecord[]>();
|
||||
|
||||
function clearManifestMetadataCache(): void {
|
||||
manifestMetadataCache = new WeakMap();
|
||||
}
|
||||
|
||||
// Manifest metadata is process-stable; install/reload owners refresh it only
|
||||
// through the shared plugin metadata lifecycle boundary.
|
||||
registerPluginMetadataProcessMemoLifecycleClear(clearManifestMetadataCache);
|
||||
|
||||
function listChildPluginDirs(
|
||||
root: string | undefined,
|
||||
@@ -88,16 +92,6 @@ function readManifestObject(pluginDir: string): Record<string, unknown> | undefi
|
||||
return readJsonObject(path.join(pluginDir, PLUGIN_MANIFEST_FILENAME));
|
||||
}
|
||||
|
||||
function manifestFileFingerprint(pluginDir: string): string {
|
||||
const manifestPath = path.join(pluginDir, PLUGIN_MANIFEST_FILENAME);
|
||||
try {
|
||||
const stat = fs.statSync(manifestPath);
|
||||
return `${manifestPath}:${stat.mtimeMs}:${stat.size}`;
|
||||
} catch {
|
||||
return `${manifestPath}:missing`;
|
||||
}
|
||||
}
|
||||
|
||||
function listPersistedIndexPluginDirs(env: NodeJS.ProcessEnv, startOrder: number): CandidateDir[] {
|
||||
const index = readPersistedInstalledPluginIndexSync({ env });
|
||||
if (!index) {
|
||||
@@ -174,6 +168,10 @@ function uniqueCandidateDirs(candidates: CandidateDir[]): CandidateDir[] {
|
||||
export function listOpenClawPluginManifestMetadata(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): PluginManifestMetadataRecord[] {
|
||||
const cached = manifestMetadataCache.get(env);
|
||||
if (cached) {
|
||||
return cached.slice();
|
||||
}
|
||||
const candidates: CandidateDir[] = [];
|
||||
let order = 0;
|
||||
candidates.push(...listPersistedIndexPluginDirs(env, order));
|
||||
@@ -185,21 +183,7 @@ export function listOpenClawPluginManifestMetadata(
|
||||
candidates.push(
|
||||
...listChildPluginDirs(resolveDefaultPluginExtensionsDir(env), 4, order, "global"),
|
||||
);
|
||||
|
||||
const uniqueCandidates = uniqueCandidateDirs(candidates);
|
||||
const cacheKey = JSON.stringify(
|
||||
uniqueCandidates.map((candidate) => [
|
||||
candidate.pluginDir,
|
||||
candidate.rank,
|
||||
candidate.order,
|
||||
candidate.origin ?? "",
|
||||
manifestFileFingerprint(candidate.pluginDir),
|
||||
]),
|
||||
);
|
||||
if (manifestMetadataCache?.key === cacheKey) {
|
||||
return manifestMetadataCache.records.slice();
|
||||
}
|
||||
|
||||
const byManifestId = new Map<string, CandidateDir>();
|
||||
const records: PluginManifestMetadataRecord[] = [];
|
||||
for (const candidate of uniqueCandidates) {
|
||||
@@ -217,6 +201,6 @@ export function listOpenClawPluginManifestMetadata(
|
||||
}
|
||||
records.push({ pluginDir: candidate.pluginDir, manifest, origin: candidate.origin });
|
||||
}
|
||||
manifestMetadataCache = { key: cacheKey, records };
|
||||
return records;
|
||||
manifestMetadataCache.set(env, records);
|
||||
return records.slice();
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("manifest model id normalization", () => {
|
||||
expect(normalizeDemoModel()).toBe("charlie/demo-model");
|
||||
});
|
||||
|
||||
it("reuses manifest metadata while file fingerprints are unchanged", () => {
|
||||
it("reuses manifest metadata for the same environment identity", () => {
|
||||
const stateDir = tempDirs.make("openclaw-model-id-normalization-");
|
||||
const pluginDir = path.join(stateDir, "extensions", "normalizer");
|
||||
writeInstallIndex({ stateDir, pluginDir });
|
||||
|
||||
Reference in New Issue
Block a user