

























@@ -2,6 +2,8 @@ import fs from "node:fs";
22import path from "node:path";
33import type { OpenClawConfig } from "../config/types.openclaw.js";
44import type { PluginCandidate } from "./discovery.js";
5+import { hashJson } from "./installed-plugin-index-hash.js";
6+import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
57import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
68import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
79import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
@@ -13,6 +15,160 @@ import {
1315type PackageManifest,
1416} from "./manifest.js";
151718+const INSTALLED_MANIFEST_REGISTRY_CACHE_MAX_ENTRIES = 64;
19+20+type InstalledManifestRegistryCacheEntry = {
21+registry: PluginManifestRegistry;
22+lastUsed: number;
23+};
24+25+const installedManifestRegistryCache = new Map<string, InstalledManifestRegistryCacheEntry>();
26+let installedManifestRegistryCacheTick = 0;
27+28+function normalizePluginIdFilter(pluginIds: readonly string[] | undefined): string[] | undefined {
29+if (!pluginIds?.length) {
30+return undefined;
31+}
32+return [...new Set(pluginIds)].toSorted((left, right) => left.localeCompare(right));
33+}
34+35+function resolvePackageJsonPath(record: InstalledPluginIndexRecord): string | undefined {
36+if (!record.packageJson?.path) {
37+return undefined;
38+}
39+const rootDir = resolveInstalledPluginRootDir(record);
40+const packageJsonPath = path.resolve(rootDir, record.packageJson.path);
41+const relative = path.relative(rootDir, packageJsonPath);
42+if (relative.startsWith("..") || path.isAbsolute(relative)) {
43+return undefined;
44+}
45+return packageJsonPath;
46+}
47+48+function safeFileSignature(filePath: string | undefined): string | undefined {
49+if (!filePath) {
50+return undefined;
51+}
52+try {
53+const stat = fs.statSync(filePath);
54+return `${filePath}:${stat.size}:${stat.mtimeMs}`;
55+} catch {
56+return `${filePath}:missing`;
57+}
58+}
59+60+function shouldUseInstalledManifestRegistryCache(params: {
61+env: NodeJS.ProcessEnv;
62+bundledChannelConfigCollector?: BundledChannelConfigCollector;
63+}): boolean {
64+if (params.bundledChannelConfigCollector) {
65+return false;
66+}
67+if (params.env.OPENCLAW_DISABLE_INSTALLED_PLUGIN_MANIFEST_REGISTRY_CACHE?.trim()) {
68+return false;
69+}
70+return !params.env.OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE?.trim();
71+}
72+73+function buildInstalledManifestRegistryCacheKey(params: {
74+index: InstalledPluginIndex;
75+config?: OpenClawConfig;
76+workspaceDir?: string;
77+env: NodeJS.ProcessEnv;
78+pluginIds?: readonly string[];
79+includeDisabled?: boolean;
80+}): string {
81+return hashJson({
82+index: {
83+version: params.index.version,
84+hostContractVersion: params.index.hostContractVersion,
85+compatRegistryVersion: params.index.compatRegistryVersion,
86+migrationVersion: params.index.migrationVersion,
87+policyHash: params.index.policyHash,
88+installRecords: params.index.installRecords,
89+diagnostics: params.index.diagnostics,
90+plugins: params.index.plugins.map((record) => {
91+const packageJsonPath = resolvePackageJsonPath(record);
92+return {
93+pluginId: record.pluginId,
94+packageName: record.packageName,
95+packageVersion: record.packageVersion,
96+installRecord: record.installRecord,
97+installRecordHash: record.installRecordHash,
98+packageInstall: record.packageInstall,
99+packageChannel: record.packageChannel,
100+manifestPath: record.manifestPath,
101+manifestHash: record.manifestHash,
102+manifestFile: safeFileSignature(record.manifestPath),
103+format: record.format,
104+bundleFormat: record.bundleFormat,
105+source: record.source,
106+setupSource: record.setupSource,
107+packageJson: record.packageJson,
108+packageJsonFile: safeFileSignature(packageJsonPath),
109+rootDir: record.rootDir,
110+origin: record.origin,
111+enabled: record.enabled,
112+enabledByDefault: record.enabledByDefault,
113+syntheticAuthRefs: record.syntheticAuthRefs,
114+startup: record.startup,
115+compat: record.compat,
116+};
117+}),
118+},
119+request: {
120+workspaceDir: params.workspaceDir,
121+pluginIds: normalizePluginIdFilter(params.pluginIds),
122+includeDisabled: params.includeDisabled === true,
123+configPolicyHash: resolveInstalledPluginIndexPolicyHash(params.config),
124+env: {
125+OPENCLAW_VERSION: params.env.OPENCLAW_VERSION,
126+HOME: params.env.HOME,
127+USERPROFILE: params.env.USERPROFILE,
128+},
129+},
130+});
131+}
132+133+function getCachedInstalledManifestRegistry(cacheKey: string): PluginManifestRegistry | undefined {
134+const cached = installedManifestRegistryCache.get(cacheKey);
135+if (!cached) {
136+return undefined;
137+}
138+cached.lastUsed = ++installedManifestRegistryCacheTick;
139+return cached.registry;
140+}
141+142+function setCachedInstalledManifestRegistry(
143+cacheKey: string,
144+registry: PluginManifestRegistry,
145+): void {
146+if (
147+!installedManifestRegistryCache.has(cacheKey) &&
148+installedManifestRegistryCache.size >= INSTALLED_MANIFEST_REGISTRY_CACHE_MAX_ENTRIES
149+) {
150+let oldestKey: string | undefined;
151+let oldestTick = Number.POSITIVE_INFINITY;
152+for (const [key, entry] of installedManifestRegistryCache) {
153+if (entry.lastUsed < oldestTick) {
154+oldestKey = key;
155+oldestTick = entry.lastUsed;
156+}
157+}
158+if (oldestKey) {
159+installedManifestRegistryCache.delete(oldestKey);
160+}
161+}
162+installedManifestRegistryCache.set(cacheKey, {
163+ registry,
164+lastUsed: ++installedManifestRegistryCacheTick,
165+});
166+}
167+168+export function clearInstalledManifestRegistryCache(): void {
169+installedManifestRegistryCache.clear();
170+}
171+16172function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
17173return record.rootDir || path.dirname(record.manifestPath || process.cwd());
18174}
@@ -94,6 +250,26 @@ export function loadPluginManifestRegistryForInstalledIndex(params: {
94250if (params.pluginIds && params.pluginIds.length === 0) {
95251return { plugins: [], diagnostics: [] };
96252}
253+const env = params.env ?? process.env;
254+const cacheKey = shouldUseInstalledManifestRegistryCache({
255+ env,
256+bundledChannelConfigCollector: params.bundledChannelConfigCollector,
257+})
258+ ? buildInstalledManifestRegistryCacheKey({
259+index: params.index,
260+config: params.config,
261+workspaceDir: params.workspaceDir,
262+ env,
263+pluginIds: params.pluginIds,
264+includeDisabled: params.includeDisabled,
265+})
266+ : undefined;
267+if (cacheKey) {
268+const cached = getCachedInstalledManifestRegistry(cacheKey);
269+if (cached) {
270+return cached;
271+}
272+}
97273const pluginIdSet = params.pluginIds?.length ? new Set(params.pluginIds) : null;
98274const diagnostics = pluginIdSet
99275 ? params.index.diagnostics.filter((diagnostic) => {
@@ -105,10 +281,10 @@ export function loadPluginManifestRegistryForInstalledIndex(params: {
105281.filter((plugin) => params.includeDisabled || plugin.enabled)
106282.filter((plugin) => !pluginIdSet || pluginIdSet.has(plugin.pluginId))
107283.map(toPluginCandidate);
108-return loadPluginManifestRegistry({
284+const registry = loadPluginManifestRegistry({
109285config: params.config,
110286workspaceDir: params.workspaceDir,
111-env: params.env,
287+ env,
112288cache: false,
113289 candidates,
114290diagnostics: [...diagnostics],
@@ -117,4 +293,8 @@ export function loadPluginManifestRegistryForInstalledIndex(params: {
117293 ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector }
118294 : {}),
119295});
296+if (cacheKey) {
297+setCachedInstalledManifestRegistry(cacheKey, registry);
298+}
299+return registry;
120300}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。