












@@ -1,9 +1,12 @@
1+import fs from "node:fs";
12import path from "node:path";
23import { MANIFEST_KEY } from "../../compat/legacy-names.js";
4+import type { PluginInstallRecord } from "../../config/types.plugins.js";
35import { tryReadJsonSync } from "../../infra/json-files.js";
46import { isPrereleaseSemverVersion, parseRegistryNpmSpec } from "../../infra/npm-registry-spec.js";
57import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
68import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
9+import type { PluginDiscoveryResult } from "../../plugins/discovery.js";
710import {
811describePluginInstallSource,
912type PluginInstallSourceInfo,
@@ -52,6 +55,8 @@ type CatalogOptions = {
5255officialCatalogPaths?: string[];
5356env?: NodeJS.ProcessEnv;
5457excludeWorkspace?: boolean;
58+installRecords?: Record<string, PluginInstallRecord>;
59+discovery?: PluginDiscoveryResult;
5560};
56615762const ORIGIN_PRIORITY: Record<PluginOrigin, number> = {
@@ -72,7 +77,13 @@ type ExternalCatalogEntry = {
72777378const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
7479const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = path.join("dist", "channel-catalog.json");
80+type CatalogEntriesCacheEntry = {
81+fingerprint: string;
82+entries: ExternalCatalogEntry[] | null;
83+};
84+7585const officialCatalogEntriesByPath = new Map<string, ExternalCatalogEntry[] | null>();
86+const externalCatalogEntriesByPath = new Map<string, CatalogEntriesCacheEntry>();
76877788type ManifestKey = typeof MANIFEST_KEY;
7889@@ -129,17 +140,43 @@ function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEnt
129140const paths = resolveExternalCatalogPaths(options).map((rawPath) =>
130141resolveUserPath(rawPath, options.env ?? process.env),
131142);
132-return loadCatalogEntriesFromPaths(paths);
143+return loadCatalogEntriesFromPaths(paths, externalCatalogEntriesByPath);
144+}
145+146+function fingerprintCatalogPath(filePath: string): string {
147+try {
148+const stat = fs.statSync(filePath, { bigint: true });
149+const kind = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
150+return [kind, stat.size.toString(), stat.mtimeNs.toString(), stat.ctimeNs.toString()].join(":");
151+} catch {
152+return "missing";
153+}
133154}
134155135-function loadCatalogEntriesFromPaths(paths: Iterable<string>): ExternalCatalogEntry[] {
156+function readCatalogEntriesFromPath(resolvedPath: string): ExternalCatalogEntry[] | null {
157+const payload = tryReadJsonSync(resolvedPath);
158+return payload === null ? null : parseCatalogEntries(payload);
159+}
160+161+function loadCatalogEntriesFromPaths(
162+paths: Iterable<string>,
163+cache?: Map<string, CatalogEntriesCacheEntry>,
164+): ExternalCatalogEntry[] {
136165const entries: ExternalCatalogEntry[] = [];
137166for (const resolvedPath of paths) {
138-const payload = tryReadJsonSync(resolvedPath);
139-if (payload === null) {
167+const fingerprint = cache ? fingerprintCatalogPath(resolvedPath) : undefined;
168+const cached = fingerprint ? cache?.get(resolvedPath) : undefined;
169+const parsed =
170+cached && cached.fingerprint === fingerprint
171+ ? cached.entries
172+ : readCatalogEntriesFromPath(resolvedPath);
173+if (cache && fingerprint) {
174+cache.set(resolvedPath, { fingerprint, entries: parsed });
175+}
176+if (parsed === null) {
140177continue;
141178}
142-entries.push(...parseCatalogEntries(payload));
179+entries.push(...parsed);
143180}
144181return entries;
145182}
@@ -399,6 +436,8 @@ export function listChannelPluginCatalogEntries(
399436const manifestEntries = listChannelCatalogEntries({
400437workspaceDir: options.workspaceDir,
401438env: options.env,
439+installRecords: options.installRecords,
440+discovery: options.discovery,
402441});
403442const resolved = new Map<string, { entry: ChannelPluginCatalogEntry; priority: number }>();
404443此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。