mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:30:42 +00:00
fix(plugins): partition bootstrap bundled root caches
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { listBundledChannelPluginIds } from "./bundled-ids.js";
|
||||
import { listBundledChannelPluginIdsForRoot } from "./bundled-ids.js";
|
||||
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
|
||||
import {
|
||||
getBundledChannelPlugin,
|
||||
getBundledChannelSecrets,
|
||||
@@ -16,7 +17,7 @@ type CachedBootstrapPlugins = {
|
||||
missingIds: Set<string>;
|
||||
};
|
||||
|
||||
let cachedBootstrapPlugins: CachedBootstrapPlugins | null = null;
|
||||
const cachedBootstrapPluginsByRoot = new Map<string, CachedBootstrapPlugins>();
|
||||
|
||||
function mergePluginSection<T>(
|
||||
runtimeValue: T | undefined,
|
||||
@@ -63,18 +64,29 @@ function mergeBootstrapPlugin(
|
||||
} as ChannelPlugin;
|
||||
}
|
||||
|
||||
function buildBootstrapPlugins(): CachedBootstrapPlugins {
|
||||
function buildBootstrapPlugins(
|
||||
packageRoot: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): CachedBootstrapPlugins {
|
||||
return {
|
||||
sortedIds: listBundledChannelPluginIds(),
|
||||
sortedIds: listBundledChannelPluginIdsForRoot(packageRoot, env),
|
||||
byId: new Map(),
|
||||
secretsById: new Map(),
|
||||
missingIds: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
function getBootstrapPlugins(): CachedBootstrapPlugins {
|
||||
cachedBootstrapPlugins ??= buildBootstrapPlugins();
|
||||
return cachedBootstrapPlugins;
|
||||
function getBootstrapPlugins(
|
||||
packageRoot = resolveBundledChannelPackageRoot(),
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): CachedBootstrapPlugins {
|
||||
const cached = cachedBootstrapPluginsByRoot.get(packageRoot);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const created = buildBootstrapPlugins(packageRoot, env);
|
||||
cachedBootstrapPluginsByRoot.set(packageRoot, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
export function listBootstrapChannelPluginIds(): readonly string[] {
|
||||
@@ -99,7 +111,7 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi
|
||||
if (!resolvedId) {
|
||||
return undefined;
|
||||
}
|
||||
const registry = getBootstrapPlugins();
|
||||
const registry = getBootstrapPlugins(resolveBundledChannelPackageRoot());
|
||||
const cached = registry.byId.get(resolvedId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
@@ -126,7 +138,7 @@ export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secret
|
||||
if (!resolvedId) {
|
||||
return undefined;
|
||||
}
|
||||
const registry = getBootstrapPlugins();
|
||||
const registry = getBootstrapPlugins(resolveBundledChannelPackageRoot());
|
||||
const cached = registry.secretsById.get(resolvedId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
@@ -142,5 +154,5 @@ export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secret
|
||||
}
|
||||
|
||||
export function clearBootstrapChannelPluginCache(): void {
|
||||
cachedBootstrapPlugins = null;
|
||||
cachedBootstrapPluginsByRoot.clear();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
|
||||
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
|
||||
|
||||
let bundledChannelPluginIds: string[] | null = null;
|
||||
const bundledChannelPluginIdsByRoot = new Map<string, string[]>();
|
||||
|
||||
export function listBundledChannelPluginIds(): string[] {
|
||||
bundledChannelPluginIds ??= listChannelCatalogEntries({ origin: "bundled" })
|
||||
export function listBundledChannelPluginIdsForRoot(
|
||||
packageRoot: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const cached = bundledChannelPluginIdsByRoot.get(packageRoot);
|
||||
if (cached) {
|
||||
return [...cached];
|
||||
}
|
||||
const loaded = listChannelCatalogEntries({ origin: "bundled", env })
|
||||
.map((entry) => entry.pluginId)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return [...bundledChannelPluginIds];
|
||||
bundledChannelPluginIdsByRoot.set(packageRoot, loaded);
|
||||
return [...loaded];
|
||||
}
|
||||
|
||||
export function listBundledChannelPluginIds(): string[] {
|
||||
return listBundledChannelPluginIdsForRoot(resolveBundledChannelPackageRoot());
|
||||
}
|
||||
|
||||
129
src/channels/plugins/bundled-root-caches.test.ts
Normal file
129
src/channels/plugins/bundled-root-caches.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.ts";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
function makeBundledRoot(prefix: string): { root: string; pluginsDir: string } {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(root);
|
||||
const pluginsDir = path.join(root, "dist", "extensions");
|
||||
fs.mkdirSync(pluginsDir, { recursive: true });
|
||||
return { root, pluginsDir };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
vi.resetModules();
|
||||
vi.doUnmock("../../plugins/channel-catalog-registry.js");
|
||||
vi.doUnmock("./bundled.js");
|
||||
vi.doUnmock("./bundled-ids.js");
|
||||
});
|
||||
|
||||
describe("bundled root-aware caches", () => {
|
||||
it("partitions bundled channel ids by active bundled root without re-importing", async () => {
|
||||
const rootA = makeBundledRoot("openclaw-bundled-ids-a-");
|
||||
const rootB = makeBundledRoot("openclaw-bundled-ids-b-");
|
||||
|
||||
vi.doMock("../../plugins/channel-catalog-registry.js", () => ({
|
||||
listChannelCatalogEntries: (params?: { env?: NodeJS.ProcessEnv }) => {
|
||||
const activeRoot = params?.env?.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
if (activeRoot === rootA.pluginsDir) {
|
||||
return [{ pluginId: "alpha" }];
|
||||
}
|
||||
if (activeRoot === rootB.pluginsDir) {
|
||||
return [{ pluginId: "beta" }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}));
|
||||
|
||||
const bundledIds = await importFreshModule<typeof import("./bundled-ids.js")>(
|
||||
import.meta.url,
|
||||
"./bundled-ids.js?scope=root-aware-id-cache",
|
||||
);
|
||||
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootA.pluginsDir;
|
||||
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["alpha"]);
|
||||
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootB.pluginsDir;
|
||||
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["beta"]);
|
||||
});
|
||||
|
||||
it("partitions bootstrap plugin caches by active bundled root without re-importing", async () => {
|
||||
const rootA = makeBundledRoot("openclaw-bootstrap-a-");
|
||||
const rootB = makeBundledRoot("openclaw-bootstrap-b-");
|
||||
|
||||
vi.doMock("./bundled-ids.js", () => ({
|
||||
listBundledChannelPluginIdsForRoot: (packageRoot: string) => {
|
||||
if (packageRoot === rootA.root) {
|
||||
return ["alpha"];
|
||||
}
|
||||
if (packageRoot === rootB.root) {
|
||||
return ["beta"];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock("./bundled.js", () => ({
|
||||
getBundledChannelPlugin: (id: string) => ({
|
||||
id,
|
||||
meta: { id, label: `runtime-${id}` },
|
||||
capabilities: {},
|
||||
config: {},
|
||||
}),
|
||||
getBundledChannelSetupPlugin: (id: string) => {
|
||||
const activeRoot = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const suffix =
|
||||
activeRoot === rootA.pluginsDir ? "A" : activeRoot === rootB.pluginsDir ? "B" : "unknown";
|
||||
return {
|
||||
id,
|
||||
meta: { id, label: `setup-${suffix}` },
|
||||
capabilities: {},
|
||||
config: {},
|
||||
};
|
||||
},
|
||||
getBundledChannelSecrets: (id: string) => ({
|
||||
secretTargetRegistryEntries: [{ id: `runtime-${id}`, targetType: "channel" }],
|
||||
}),
|
||||
getBundledChannelSetupSecrets: (id: string) => {
|
||||
const activeRoot = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const suffix =
|
||||
activeRoot === rootA.pluginsDir ? "A" : activeRoot === rootB.pluginsDir ? "B" : "unknown";
|
||||
return {
|
||||
secretTargetRegistryEntries: [{ id: `setup-${id}-${suffix}`, targetType: "channel" }],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const bootstrapRegistry = await importFreshModule<typeof import("./bootstrap-registry.js")>(
|
||||
import.meta.url,
|
||||
"./bootstrap-registry.js?scope=root-aware-bootstrap-cache",
|
||||
);
|
||||
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootA.pluginsDir;
|
||||
expect(bootstrapRegistry.listBootstrapChannelPluginIds()).toEqual(["alpha"]);
|
||||
expect(bootstrapRegistry.getBootstrapChannelPlugin("alpha")?.meta.label).toBe("setup-A");
|
||||
expect(
|
||||
bootstrapRegistry.getBootstrapChannelSecrets("alpha")?.secretTargetRegistryEntries?.[0]?.id,
|
||||
).toBe("setup-alpha-A");
|
||||
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootB.pluginsDir;
|
||||
expect(bootstrapRegistry.listBootstrapChannelPluginIds()).toEqual(["beta"]);
|
||||
expect(bootstrapRegistry.getBootstrapChannelPlugin("beta")?.meta.label).toBe("setup-B");
|
||||
expect(
|
||||
bootstrapRegistry.getBootstrapChannelSecrets("beta")?.secretTargetRegistryEntries?.[0]?.id,
|
||||
).toBe("setup-beta-B");
|
||||
});
|
||||
});
|
||||
35
src/channels/plugins/bundled-root.ts
Normal file
35
src/channels/plugins/bundled-root.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
|
||||
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
moduleUrl: import.meta.url.startsWith("file:") ? import.meta.url : undefined,
|
||||
}) ??
|
||||
(import.meta.url.startsWith("file:")
|
||||
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
|
||||
: process.cwd());
|
||||
|
||||
export function derivePackageRootFromBundledPluginsDir(pluginsDir: string): string {
|
||||
const resolvedDir = path.resolve(pluginsDir);
|
||||
if (path.basename(resolvedDir) !== "extensions") {
|
||||
return resolvedDir;
|
||||
}
|
||||
const parentDir = path.dirname(resolvedDir);
|
||||
const parentBase = path.basename(parentDir);
|
||||
if (parentBase === "dist" || parentBase === "dist-runtime") {
|
||||
return path.dirname(parentDir);
|
||||
}
|
||||
return parentDir;
|
||||
}
|
||||
|
||||
export function resolveBundledChannelPackageRoot(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const bundledPluginsDir = resolveBundledPluginsDir(env);
|
||||
if (bundledPluginsDir) {
|
||||
return derivePackageRootFromBundledPluginsDir(bundledPluginsDir);
|
||||
}
|
||||
return OPENCLAW_PACKAGE_ROOT;
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
listBundledChannelPluginMetadata,
|
||||
resolveBundledChannelGeneratedPath,
|
||||
type BundledChannelPluginMetadata,
|
||||
} from "../../plugins/bundled-channel-runtime.js";
|
||||
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
|
||||
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
|
||||
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
||||
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
|
||||
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
|
||||
import type { ChannelPlugin } from "./types.plugin.js";
|
||||
import type { ChannelId } from "./types.public.js";
|
||||
@@ -54,36 +52,6 @@ type BundledChannelCacheContext = {
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("channels");
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
moduleUrl: import.meta.url.startsWith("file:") ? import.meta.url : undefined,
|
||||
}) ??
|
||||
(import.meta.url.startsWith("file:")
|
||||
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
|
||||
: process.cwd());
|
||||
|
||||
function derivePackageRootFromBundledPluginsDir(pluginsDir: string): string {
|
||||
const resolvedDir = path.resolve(pluginsDir);
|
||||
if (path.basename(resolvedDir) !== "extensions") {
|
||||
return resolvedDir;
|
||||
}
|
||||
const parentDir = path.dirname(resolvedDir);
|
||||
const parentBase = path.basename(parentDir);
|
||||
if (parentBase === "dist" || parentBase === "dist-runtime") {
|
||||
return path.dirname(parentDir);
|
||||
}
|
||||
return parentDir;
|
||||
}
|
||||
|
||||
function resolveBundledChannelPackageRoot(): string {
|
||||
const bundledPluginsDir = resolveBundledPluginsDir(process.env);
|
||||
if (bundledPluginsDir) {
|
||||
return derivePackageRootFromBundledPluginsDir(bundledPluginsDir);
|
||||
}
|
||||
return OPENCLAW_PACKAGE_ROOT;
|
||||
}
|
||||
|
||||
function resolveChannelPluginModuleEntry(
|
||||
moduleExport: unknown,
|
||||
|
||||
Reference in New Issue
Block a user