






















@@ -2,12 +2,14 @@ import crypto from "node:crypto";
22import fs from "node:fs";
33import path from "node:path";
44import { resolveUserPath } from "../utils.js";
5+import { resolveCompatibilityHostVersion } from "../version.js";
56import { resolveBundledPluginsDir } from "./bundled-dir.js";
67import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
78import type { PluginDiscoveryResult } from "./discovery.js";
8-import { fileSignatureMatches } from "./installed-plugin-index-hash.js";
9+import { fileSignatureMatches, hashJson } from "./installed-plugin-index-hash.js";
910import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js";
1011import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js";
12+import { resolveInstalledPluginIndexStorePath } from "./installed-plugin-index-store-path.js";
1113import {
1214inspectPersistedInstalledPluginIndex,
1315readPersistedInstalledPluginIndexSync,
@@ -27,6 +29,7 @@ import {
2729type LoadInstalledPluginIndexParams,
2830type RefreshInstalledPluginIndexParams,
2931} from "./installed-plugin-index.js";
32+import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
3033import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js";
31343235export type PluginRegistrySnapshot = InstalledPluginIndex;
@@ -53,6 +56,35 @@ export type PluginRegistrySnapshotResult = {
5356};
54575558export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY";
59+const MAX_PLUGIN_REGISTRY_SNAPSHOT_MEMOS = 8;
60+const REGISTRY_SNAPSHOT_MEMO_ENV_KEYS = [
61+"APPDATA",
62+"HOME",
63+"OPENCLAW_BUNDLED_PLUGINS_DIR",
64+"OPENCLAW_COMPATIBILITY_HOST_VERSION",
65+"OPENCLAW_CONFIG_PATH",
66+"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
67+"OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS",
68+DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV,
69+"OPENCLAW_HOME",
70+"OPENCLAW_NIX_MODE",
71+"OPENCLAW_STATE_DIR",
72+"USERPROFILE",
73+"XDG_CONFIG_HOME",
74+] as const;
75+76+type PluginRegistrySnapshotMemo = {
77+key: string;
78+result: PluginRegistrySnapshotResult;
79+};
80+81+let pluginRegistrySnapshotMemos: PluginRegistrySnapshotMemo[] = [];
82+83+function clearLoadPluginRegistrySnapshotMemo(): void {
84+pluginRegistrySnapshotMemos = [];
85+}
86+87+registerPluginMetadataProcessMemoLifecycleClear(clearLoadPluginRegistrySnapshotMemo);
56885789function formatDeprecatedPersistedRegistryDisableWarning(): string {
5890return `${DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV} is a deprecated break-glass compatibility switch; use \`openclaw plugins registry --refresh\` or \`openclaw doctor --fix\` to repair registry state.`;
@@ -73,6 +105,96 @@ function hasEnvFlag(env: NodeJS.ProcessEnv, name: string): boolean {
73105return Boolean(value && value !== "0" && value !== "false" && value !== "no");
74106}
75107108+function pickRegistrySnapshotMemoEnv(env: NodeJS.ProcessEnv): Record<string, string> {
109+return Object.fromEntries(
110+REGISTRY_SNAPSHOT_MEMO_ENV_KEYS.flatMap((key) => {
111+const value = env[key];
112+return value === undefined ? [] : [[key, value]];
113+}),
114+);
115+}
116+117+function canMemoizePluginRegistrySnapshot(params: LoadPluginRegistryParams): boolean {
118+return (
119+params.index === undefined &&
120+params.candidates === undefined &&
121+params.diagnostics === undefined &&
122+params.discovery === undefined &&
123+params.installRecords === undefined &&
124+params.now === undefined &&
125+params.filePath === undefined &&
126+params.pluginIndexFilePath === undefined
127+);
128+}
129+130+function resolvePluginRegistrySnapshotMemoKey(
131+params: LoadPluginRegistryParams,
132+env: NodeJS.ProcessEnv,
133+): string | undefined {
134+if (!canMemoizePluginRegistrySnapshot(params)) {
135+return undefined;
136+}
137+return hashJson({
138+config: params.config ?? null,
139+cwd: process.cwd(),
140+env: pickRegistrySnapshotMemoEnv(env),
141+hostContractVersion: resolveCompatibilityHostVersion(env),
142+preferPersisted: params.preferPersisted ?? null,
143+// Plugin manifests are process-stable inside the Gateway, while the persisted
144+// registry envelope can change through explicit refresh/install flows.
145+registryFile: fileFingerprint(
146+resolveInstalledPluginIndexStorePath({
147+ env,
148+ ...(params.stateDir ? { stateDir: params.stateDir } : {}),
149+}),
150+),
151+stateDir: params.stateDir ? resolveUserPath(params.stateDir, env) : null,
152+workspaceDir: params.workspaceDir ? resolveUserPath(params.workspaceDir, env) : null,
153+});
154+}
155+156+function fileFingerprint(filePath: string): unknown {
157+try {
158+const stat = fs.statSync(filePath, { bigint: true });
159+const kind = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
160+return [filePath, kind, stat.size.toString(), stat.mtimeNs.toString(), stat.ctimeNs.toString()];
161+} catch {
162+return [filePath, "missing"];
163+}
164+}
165+166+function findPluginRegistrySnapshotMemo(
167+key: string | undefined,
168+): PluginRegistrySnapshotResult | undefined {
169+if (!key) {
170+return undefined;
171+}
172+const index = pluginRegistrySnapshotMemos.findIndex((memo) => memo.key === key);
173+if (index === -1) {
174+return undefined;
175+}
176+const [memo] = pluginRegistrySnapshotMemos.splice(index, 1);
177+if (!memo) {
178+return undefined;
179+}
180+pluginRegistrySnapshotMemos.unshift(memo);
181+return memo.result;
182+}
183+184+function rememberPluginRegistrySnapshotMemo(
185+key: string | undefined,
186+result: PluginRegistrySnapshotResult,
187+): PluginRegistrySnapshotResult {
188+if (!key) {
189+return result;
190+}
191+pluginRegistrySnapshotMemos = [
192+{ key, result },
193+ ...pluginRegistrySnapshotMemos.filter((memo) => memo.key !== key),
194+].slice(0, MAX_PLUGIN_REGISTRY_SNAPSHOT_MEMOS);
195+return result;
196+}
197+76198function canReuseCurrentPluginMetadataSnapshot(params: LoadPluginRegistryParams): boolean {
77199return (
78200params.preferPersisted !== false &&
@@ -288,6 +410,11 @@ export function loadPluginRegistrySnapshotWithMetadata(
288410}
289411290412const env = params.env ?? process.env;
413+const memoKey = resolvePluginRegistrySnapshotMemoKey(params, env);
414+const memo = findPluginRegistrySnapshotMemo(memoKey);
415+if (memo) {
416+return memo;
417+}
291418const diagnostics: PluginRegistrySnapshotDiagnostic[] = [];
292419const disabledByCaller = params.preferPersisted === false;
293420const disabledByEnv = hasEnvFlag(env, DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV);
@@ -354,7 +481,7 @@ export function loadPluginRegistrySnapshotWithMetadata(
354481source: "persisted",
355482 diagnostics,
356483};
357-return persistedResult;
484+return rememberPluginRegistrySnapshotMemo(memoKey, persistedResult);
358485}
359486} else if (persistedReadsEnabled) {
360487diagnostics.push({
@@ -379,12 +506,12 @@ export function loadPluginRegistrySnapshotWithMetadata(
379506 ? params.installRecords
380507 : (params.installRecords ?? {}),
381508});
382-return {
509+return rememberPluginRegistrySnapshotMemo(memoKey, {
383510snapshot: derived.index,
384511source: "derived",
385512 diagnostics,
386513discovery: derived.discovery,
387-};
514+});
388515}
389516390517function resolveSnapshot(params: LoadPluginRegistryParams = {}): PluginRegistrySnapshot {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。