mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
perf: cache bundled runtime dep manifests
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
isWritableDirectory,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDepsNpmRunner,
|
||||
scanBundledPluginRuntimeDeps,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
|
||||
@@ -41,6 +42,30 @@ function writeInstalledPackage(rootDir: string, packageName: string, version: st
|
||||
);
|
||||
}
|
||||
|
||||
function writeBundledPluginPackage(params: {
|
||||
packageRoot: string;
|
||||
pluginId: string;
|
||||
deps: Record<string, string>;
|
||||
enabledByDefault?: boolean;
|
||||
channels?: string[];
|
||||
}): string {
|
||||
const pluginRoot = path.join(params.packageRoot, "dist", "extensions", params.pluginId);
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify({ dependencies: params.deps }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: params.pluginId,
|
||||
enabledByDefault: params.enabledByDefault === true,
|
||||
...(params.channels ? { channels: params.channels } : {}),
|
||||
}),
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
function statfsFixture(params: {
|
||||
bavail: number;
|
||||
bsize?: number;
|
||||
@@ -587,6 +612,116 @@ describe("installBundledRuntimeDeps", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanBundledPluginRuntimeDeps config policy", () => {
|
||||
function setupPolicyPackageRoot(): string {
|
||||
const packageRoot = makeTempDir();
|
||||
writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "alpha",
|
||||
deps: { "alpha-runtime": "1.0.0" },
|
||||
enabledByDefault: true,
|
||||
});
|
||||
writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "telegram",
|
||||
deps: { "telegram-runtime": "2.0.0" },
|
||||
channels: ["telegram"],
|
||||
});
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "includes default-enabled bundled plugins",
|
||||
config: {},
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: ["alpha-runtime@1.0.0"],
|
||||
},
|
||||
{
|
||||
name: "keeps default-enabled bundled plugins behind restrictive allowlists",
|
||||
config: { plugins: { allow: ["browser"] } },
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: [],
|
||||
},
|
||||
{
|
||||
name: "does not let explicit plugin entries bypass restrictive allowlists",
|
||||
config: { plugins: { allow: ["browser"], entries: { alpha: { enabled: true } } } },
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: [],
|
||||
},
|
||||
{
|
||||
name: "lets deny override default-enabled bundled plugins",
|
||||
config: { plugins: { deny: ["alpha"] } },
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: [],
|
||||
},
|
||||
{
|
||||
name: "lets disabled entries override default-enabled bundled plugins",
|
||||
config: { plugins: { entries: { alpha: { enabled: false } } } },
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: [],
|
||||
},
|
||||
{
|
||||
name: "lets explicit bundled channel enablement bypass restrictive allowlists",
|
||||
config: {
|
||||
plugins: { allow: ["browser"] },
|
||||
channels: { telegram: { enabled: true } },
|
||||
},
|
||||
includeConfiguredChannels: false,
|
||||
expectedDeps: ["telegram-runtime@2.0.0"],
|
||||
},
|
||||
{
|
||||
name: "keeps channel recovery behind restrictive allowlists",
|
||||
config: {
|
||||
plugins: { allow: ["browser"] },
|
||||
channels: { telegram: { botToken: "123:abc" } },
|
||||
},
|
||||
includeConfiguredChannels: true,
|
||||
expectedDeps: [],
|
||||
},
|
||||
{
|
||||
name: "includes configured channels during recovery without restrictive allowlists",
|
||||
config: { channels: { telegram: { botToken: "123:abc" } } },
|
||||
includeConfiguredChannels: true,
|
||||
expectedDeps: ["alpha-runtime@1.0.0", "telegram-runtime@2.0.0"],
|
||||
},
|
||||
{
|
||||
name: "lets explicit channel disable override recovery",
|
||||
config: { channels: { telegram: { botToken: "123:abc", enabled: false } } },
|
||||
includeConfiguredChannels: true,
|
||||
expectedDeps: ["alpha-runtime@1.0.0"],
|
||||
},
|
||||
])("$name", ({ config, includeConfiguredChannels, expectedDeps }) => {
|
||||
const result = scanBundledPluginRuntimeDeps({
|
||||
packageRoot: setupPolicyPackageRoot(),
|
||||
config,
|
||||
includeConfiguredChannels,
|
||||
});
|
||||
|
||||
expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual(expectedDeps);
|
||||
expect(result.conflicts).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads each bundled plugin manifest once per runtime-deps scan", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const pluginRoot = writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "alpha",
|
||||
deps: { "alpha-runtime": "1.0.0" },
|
||||
enabledByDefault: true,
|
||||
channels: ["alpha"],
|
||||
});
|
||||
const manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
|
||||
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
|
||||
|
||||
scanBundledPluginRuntimeDeps({ packageRoot, config: {} });
|
||||
|
||||
expect(
|
||||
readFileSyncSpy.mock.calls.filter((call) => path.resolve(String(call[0])) === manifestPath),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
it("installs plugin-local runtime deps when one is missing", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
|
||||
@@ -877,17 +877,31 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
|
||||
},
|
||||
};
|
||||
}
|
||||
function readBundledPluginChannels(pluginDir: string): string[] {
|
||||
type BundledPluginRuntimeDepsManifest = {
|
||||
channels: string[];
|
||||
enabledByDefault: boolean;
|
||||
};
|
||||
|
||||
type BundledPluginRuntimeDepsManifestCache = Map<string, BundledPluginRuntimeDepsManifest>;
|
||||
|
||||
function readBundledPluginRuntimeDepsManifest(
|
||||
pluginDir: string,
|
||||
cache?: BundledPluginRuntimeDepsManifestCache,
|
||||
): BundledPluginRuntimeDepsManifest {
|
||||
const cached = cache?.get(pluginDir);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const manifest = readJsonObject(path.join(pluginDir, "openclaw.plugin.json"));
|
||||
const channels = manifest?.channels;
|
||||
if (!Array.isArray(channels)) {
|
||||
return [];
|
||||
}
|
||||
return channels.filter((entry): entry is string => typeof entry === "string" && entry !== "");
|
||||
}
|
||||
|
||||
function readBundledPluginEnabledByDefault(pluginDir: string): boolean {
|
||||
return readJsonObject(path.join(pluginDir, "openclaw.plugin.json"))?.enabledByDefault === true;
|
||||
const runtimeDepsManifest = {
|
||||
channels: Array.isArray(channels)
|
||||
? channels.filter((entry): entry is string => typeof entry === "string" && entry !== "")
|
||||
: [],
|
||||
enabledByDefault: manifest?.enabledByDefault === true,
|
||||
};
|
||||
cache?.set(pluginDir, runtimeDepsManifest);
|
||||
return runtimeDepsManifest;
|
||||
}
|
||||
|
||||
function isBundledPluginConfiguredForRuntimeDeps(params: {
|
||||
@@ -895,6 +909,7 @@ function isBundledPluginConfiguredForRuntimeDeps(params: {
|
||||
pluginId: string;
|
||||
pluginDir: string;
|
||||
includeConfiguredChannels?: boolean;
|
||||
manifestCache?: BundledPluginRuntimeDepsManifestCache;
|
||||
}): boolean {
|
||||
const plugins = normalizePluginsConfig(params.config.plugins);
|
||||
if (!plugins.enabled) {
|
||||
@@ -909,7 +924,8 @@ function isBundledPluginConfiguredForRuntimeDeps(params: {
|
||||
}
|
||||
let hasExplicitChannelDisable = false;
|
||||
let hasConfiguredChannel = false;
|
||||
for (const channelId of readBundledPluginChannels(params.pluginDir)) {
|
||||
const manifest = readBundledPluginRuntimeDepsManifest(params.pluginDir, params.manifestCache);
|
||||
for (const channelId of manifest.channels) {
|
||||
const normalizedChannelId = normalizeOptionalLowercaseString(channelId);
|
||||
if (!normalizedChannelId) {
|
||||
continue;
|
||||
@@ -955,7 +971,7 @@ function isBundledPluginConfiguredForRuntimeDeps(params: {
|
||||
if (hasConfiguredChannel) {
|
||||
return true;
|
||||
}
|
||||
return readBundledPluginEnabledByDefault(params.pluginDir);
|
||||
return manifest.enabledByDefault;
|
||||
}
|
||||
|
||||
function shouldIncludeBundledPluginRuntimeDeps(params: {
|
||||
@@ -964,6 +980,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: {
|
||||
pluginId: string;
|
||||
pluginDir: string;
|
||||
includeConfiguredChannels?: boolean;
|
||||
manifestCache?: BundledPluginRuntimeDepsManifestCache;
|
||||
}): boolean {
|
||||
if (params.pluginIds && !params.pluginIds.has(params.pluginId)) {
|
||||
return false;
|
||||
@@ -976,6 +993,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: {
|
||||
pluginId: params.pluginId,
|
||||
pluginDir: params.pluginDir,
|
||||
includeConfiguredChannels: params.includeConfiguredChannels,
|
||||
manifestCache: params.manifestCache,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -989,6 +1007,7 @@ function collectBundledPluginRuntimeDeps(params: {
|
||||
conflicts: RuntimeDepConflict[];
|
||||
} {
|
||||
const versionMap = new Map<string, Map<string, Set<string>>>();
|
||||
const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map();
|
||||
|
||||
for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
@@ -1003,6 +1022,7 @@ function collectBundledPluginRuntimeDeps(params: {
|
||||
pluginId,
|
||||
pluginDir,
|
||||
includeConfiguredChannels: params.includeConfiguredChannels,
|
||||
manifestCache,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user