

























@@ -1,5 +1,8 @@
1+import fs from "node:fs";
2+import path from "node:path";
13import type { PluginInstallRecord } from "../config/types.plugins.js";
24import { readJsonFile, readJsonFileSync } from "../infra/json-files.js";
5+import { resolveDefaultPluginNpmDir, validatePluginId } from "./install-paths.js";
36import {
47resolveInstalledPluginIndexStorePath,
58type InstalledPluginIndexStoreOptions,
@@ -30,6 +33,111 @@ function readRecordMap(value: unknown): Record<string, PluginInstallRecord> | nu
3033return records;
3134}
323536+function readJsonObjectFileSync(filePath: string): Record<string, unknown> | null {
37+try {
38+const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
39+return isRecord(parsed) ? parsed : null;
40+} catch {
41+return null;
42+}
43+}
44+45+function readStringRecord(value: unknown): Record<string, string> {
46+if (!isRecord(value)) {
47+return {};
48+}
49+const record: Record<string, string> = {};
50+for (const [key, raw] of Object.entries(value).toSorted(([left], [right]) =>
51+left.localeCompare(right),
52+)) {
53+if (typeof raw === "string" && raw.trim()) {
54+record[key] = raw.trim();
55+}
56+}
57+return record;
58+}
59+60+function hasPackagePluginMetadata(manifest: Record<string, unknown>): boolean {
61+const openclaw = manifest.openclaw;
62+if (!isRecord(openclaw)) {
63+return false;
64+}
65+const extensions = openclaw.extensions;
66+return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string");
67+}
68+69+function readManifestPluginId(packageDir: string): string | undefined {
70+const manifest = readJsonObjectFileSync(path.join(packageDir, "openclaw.plugin.json"));
71+const id = typeof manifest?.id === "string" ? manifest.id.trim() : "";
72+return id || undefined;
73+}
74+75+function resolveRecoveredManagedNpmPluginId(params: {
76+packageName: string;
77+packageDir: string;
78+}): string | undefined {
79+const packageManifest = readJsonObjectFileSync(path.join(params.packageDir, "package.json"));
80+if (!packageManifest || !hasPackagePluginMetadata(packageManifest)) {
81+return undefined;
82+}
83+const packageName =
84+typeof packageManifest.name === "string" && packageManifest.name.trim()
85+ ? packageManifest.name.trim()
86+ : params.packageName;
87+const pluginId = readManifestPluginId(params.packageDir) ?? packageName;
88+return validatePluginId(pluginId) ? undefined : pluginId;
89+}
90+91+function buildRecoveredManagedNpmInstallRecords(
92+options: InstalledPluginIndexStoreOptions = {},
93+): Record<string, PluginInstallRecord> {
94+const npmRoot = options.stateDir
95+ ? path.join(options.stateDir, "npm")
96+ : resolveDefaultPluginNpmDir(options.env);
97+const rootManifest = readJsonObjectFileSync(path.join(npmRoot, "package.json"));
98+const dependencies = readStringRecord(rootManifest?.dependencies);
99+const records: Record<string, PluginInstallRecord> = {};
100+for (const [packageName, dependencySpec] of Object.entries(dependencies)) {
101+const packageDir = path.join(npmRoot, "node_modules", packageName);
102+let stat: fs.Stats;
103+try {
104+stat = fs.statSync(packageDir);
105+} catch {
106+continue;
107+}
108+if (!stat.isDirectory()) {
109+continue;
110+}
111+const pluginId = resolveRecoveredManagedNpmPluginId({ packageName, packageDir });
112+if (!pluginId) {
113+continue;
114+}
115+const packageManifest = readJsonObjectFileSync(path.join(packageDir, "package.json"));
116+const version =
117+typeof packageManifest?.version === "string" && packageManifest.version.trim()
118+ ? packageManifest.version.trim()
119+ : undefined;
120+records[pluginId] = {
121+source: "npm",
122+spec: `${packageName}@${dependencySpec}`,
123+installPath: packageDir,
124+ ...(version ? { version, resolvedName: packageName, resolvedVersion: version } : {}),
125+ ...(version ? { resolvedSpec: `${packageName}@${version}` } : {}),
126+};
127+}
128+return records;
129+}
130+131+function mergeRecoveredManagedNpmInstallRecords(
132+persisted: Record<string, PluginInstallRecord> | null,
133+options: InstalledPluginIndexStoreOptions,
134+): Record<string, PluginInstallRecord> {
135+return {
136+ ...buildRecoveredManagedNpmInstallRecords(options),
137+ ...persisted,
138+};
139+}
140+33141function extractPluginInstallRecordsFromPersistedInstalledPluginIndex(
34142index: unknown,
35143): Record<string, PluginInstallRecord> | null {
@@ -66,11 +174,21 @@ export function readPersistedInstalledPluginIndexInstallRecordsSync(
66174export async function loadInstalledPluginIndexInstallRecords(
67175params: InstalledPluginIndexStoreOptions = {},
68176): Promise<Record<string, PluginInstallRecord>> {
69-return cloneInstallRecords((await readPersistedInstalledPluginIndexInstallRecords(params)) ?? {});
177+return cloneInstallRecords(
178+mergeRecoveredManagedNpmInstallRecords(
179+await readPersistedInstalledPluginIndexInstallRecords(params),
180+params,
181+),
182+);
70183}
7118472185export function loadInstalledPluginIndexInstallRecordsSync(
73186params: InstalledPluginIndexStoreOptions = {},
74187): Record<string, PluginInstallRecord> {
75-return cloneInstallRecords(readPersistedInstalledPluginIndexInstallRecordsSync(params) ?? {});
188+return cloneInstallRecords(
189+mergeRecoveredManagedNpmInstallRecords(
190+readPersistedInstalledPluginIndexInstallRecordsSync(params),
191+params,
192+),
193+);
76194}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。