mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:50:46 +00:00
fix: prepare bundled facade runtime deps
This commit is contained in:
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/Ollama: resolve `memorySearch.provider` custom provider ids through their configured `models.providers.<id>.api` owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as `ollama-5080` without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.
|
||||
- CLI/memory: skip eager context-window warmup for `openclaw memory` commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.
|
||||
- CLI/Telegram: route Telegram `message send` and poll actions through the running Gateway when available, so packaged installs use the staged `grammy` runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.
|
||||
- Plugins/runtime deps: prepare staged bundled plugin dependencies before loading packaged public surfaces, so OpenClaw's Telegram runtime/test facade loads resolve `grammy` from the managed runtime-deps stage without copying dependencies into the global package root. Refs #73140. Thanks @oalansilva.
|
||||
- Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead.
|
||||
- Gateway/config: let config reload continue in degraded mode when invalidity is scoped to plugin entries, so incompatible plugin configs can be skipped and the Gateway restart can still pick up the rest of the config after rollbacks. Fixes #73131. Thanks @Adam-Researchh.
|
||||
- Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to `openclaw-lark`. Fixes #56794. Thanks @wuji-tech-dev.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearBundledRuntimeDependencyNodePaths,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
} from "../plugins/bundled-runtime-deps.js";
|
||||
import { shouldExpectNativeJitiForJavaScriptTestRuntime } from "../test-utils/jiti-runtime.js";
|
||||
import {
|
||||
listImportedBundledPluginFacadeIds,
|
||||
@@ -18,6 +22,7 @@ import {
|
||||
const { createTempDirSync } = createPluginSdkTestHarness();
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
|
||||
const originalPluginStageDir = process.env.OPENCLAW_PLUGIN_STAGE_DIR;
|
||||
const FACADE_LOADER_GLOBAL = "__openclawTestLoadBundledPluginPublicSurfaceModuleSync";
|
||||
type FacadeLoaderJitiFactory = NonNullable<Parameters<typeof setFacadeLoaderJitiFactoryForTest>[0]>;
|
||||
|
||||
@@ -77,10 +82,79 @@ function createCircularPluginDir(prefix: string): string {
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
function createPackagedBundledPluginDirWithStagedRuntimeDep(prefix: string): {
|
||||
bundledPluginsDir: string;
|
||||
packageRoot: string;
|
||||
pluginRoot: string;
|
||||
stageRoot: string;
|
||||
} {
|
||||
const packageRoot = createTempDirSync(prefix);
|
||||
const pluginRoot = path.join(packageRoot, "dist", "extensions", "demo");
|
||||
const stageRoot = path.join(packageRoot, "stage");
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "0.0.0", type: "module" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/plugin-demo",
|
||||
version: "0.0.0",
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"facade-runtime-dep": "1.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "api.js"),
|
||||
[
|
||||
'import { marker as depMarker } from "facade-runtime-dep";',
|
||||
"export const marker = `facade:${depMarker}`;",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_PLUGIN_STAGE_DIR: stageRoot,
|
||||
},
|
||||
});
|
||||
const depRoot = path.join(installRoot, "node_modules", "facade-runtime-dep");
|
||||
fs.mkdirSync(depRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{ name: "facade-runtime-dep", version: "1.0.0", type: "module", exports: "./index.js" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(depRoot, "index.js"), 'export const marker = "staged";\n', "utf8");
|
||||
|
||||
return {
|
||||
bundledPluginsDir: path.join(packageRoot, "dist", "extensions"),
|
||||
packageRoot,
|
||||
pluginRoot,
|
||||
stageRoot,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resetFacadeLoaderStateForTest();
|
||||
setFacadeLoaderJitiFactoryForTest(undefined);
|
||||
clearBundledRuntimeDependencyNodePaths();
|
||||
delete (globalThis as typeof globalThis & Record<string, unknown>)[FACADE_LOADER_GLOBAL];
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
@@ -92,6 +166,11 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
|
||||
}
|
||||
if (originalPluginStageDir === undefined) {
|
||||
delete process.env.OPENCLAW_PLUGIN_STAGE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = originalPluginStageDir;
|
||||
}
|
||||
});
|
||||
|
||||
describe("plugin-sdk facade loader", () => {
|
||||
@@ -199,6 +278,22 @@ describe("plugin-sdk facade loader", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("loads built bundled public surfaces through staged runtime deps", () => {
|
||||
const fixture = createPackagedBundledPluginDirWithStagedRuntimeDep(
|
||||
"openclaw-facade-loader-runtime-deps-",
|
||||
);
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = fixture.stageRoot;
|
||||
|
||||
const loaded = loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
|
||||
dirName: "demo",
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
|
||||
expect(loaded.marker).toBe("facade:staged");
|
||||
expect(fs.existsSync(path.join(fixture.pluginRoot, "node_modules"))).toBe(false);
|
||||
});
|
||||
|
||||
it("breaks circular facade re-entry during module evaluation", () => {
|
||||
const dir = createCircularPluginDir("openclaw-facade-loader-circular-");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
|
||||
|
||||
@@ -4,6 +4,10 @@ import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import {
|
||||
isBuiltBundledPluginRuntimeRoot,
|
||||
prepareBundledPluginRuntimeRoot,
|
||||
} from "../plugins/bundled-runtime-root.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
@@ -170,46 +174,106 @@ export type FacadeModuleLocation = {
|
||||
boundaryRoot: string;
|
||||
};
|
||||
|
||||
function resolveBuiltBundledPluginRoot(params: {
|
||||
modulePath: string;
|
||||
pluginId: string;
|
||||
}): string | null {
|
||||
const resolvedModulePath = path.resolve(params.modulePath);
|
||||
let currentDir = path.dirname(resolvedModulePath);
|
||||
while (true) {
|
||||
if (
|
||||
path.basename(currentDir) === params.pluginId &&
|
||||
isBuiltBundledPluginRuntimeRoot(currentDir)
|
||||
) {
|
||||
const relativePath = path.relative(currentDir, resolvedModulePath);
|
||||
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
||||
return currentDir;
|
||||
}
|
||||
}
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareFacadeLocationForBundledRuntimeDeps(params: {
|
||||
location: FacadeModuleLocation;
|
||||
runtimeDeps?: {
|
||||
pluginId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
}): FacadeModuleLocation {
|
||||
if (!params.runtimeDeps) {
|
||||
return params.location;
|
||||
}
|
||||
const pluginRoot = resolveBuiltBundledPluginRoot({
|
||||
modulePath: params.location.modulePath,
|
||||
pluginId: params.runtimeDeps.pluginId,
|
||||
});
|
||||
if (!pluginRoot) {
|
||||
return params.location;
|
||||
}
|
||||
const prepared = prepareBundledPluginRuntimeRoot({
|
||||
pluginId: params.runtimeDeps.pluginId,
|
||||
pluginRoot,
|
||||
modulePath: params.location.modulePath,
|
||||
...(params.runtimeDeps.env ? { env: params.runtimeDeps.env } : {}),
|
||||
});
|
||||
return {
|
||||
modulePath: prepared.modulePath,
|
||||
boundaryRoot: prepared.pluginRoot,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFacadeModuleAtLocationSync<T extends object>(params: {
|
||||
location: FacadeModuleLocation;
|
||||
trackedPluginId: string | (() => string);
|
||||
runtimeDeps?: {
|
||||
pluginId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
loadModule?: (modulePath: string) => T;
|
||||
}): T {
|
||||
const cached = loadedFacadeModules.get(params.location.modulePath);
|
||||
const location = prepareFacadeLocationForBundledRuntimeDeps({
|
||||
location: params.location,
|
||||
...(params.runtimeDeps ? { runtimeDeps: params.runtimeDeps } : {}),
|
||||
});
|
||||
const cached = loadedFacadeModules.get(location.modulePath);
|
||||
if (cached) {
|
||||
return cached as T;
|
||||
}
|
||||
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: params.location.modulePath,
|
||||
rootPath: params.location.boundaryRoot,
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
boundaryLabel:
|
||||
params.location.boundaryRoot === getOpenClawPackageRoot()
|
||||
location.boundaryRoot === getOpenClawPackageRoot()
|
||||
? "OpenClaw package root"
|
||||
: (() => {
|
||||
const bundledDir = resolveBundledPluginsDir();
|
||||
return bundledDir &&
|
||||
path.resolve(params.location.boundaryRoot) === path.resolve(bundledDir)
|
||||
return bundledDir && path.resolve(location.boundaryRoot) === path.resolve(bundledDir)
|
||||
? "bundled plugin directory"
|
||||
: "plugin root";
|
||||
})(),
|
||||
rejectHardlinks: false,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error(`Unable to open bundled plugin public surface ${params.location.modulePath}`, {
|
||||
throw new Error(`Unable to open bundled plugin public surface ${location.modulePath}`, {
|
||||
cause: opened.error,
|
||||
});
|
||||
}
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
const sentinel = {} as T;
|
||||
loadedFacadeModules.set(params.location.modulePath, sentinel);
|
||||
loadedFacadeModules.set(location.modulePath, sentinel);
|
||||
|
||||
let loaded: T;
|
||||
try {
|
||||
loaded =
|
||||
params.loadModule?.(params.location.modulePath) ??
|
||||
(getJiti(params.location.modulePath)(params.location.modulePath) as T);
|
||||
params.loadModule?.(location.modulePath) ??
|
||||
(getJiti(location.modulePath)(location.modulePath) as T);
|
||||
Object.assign(sentinel, loaded);
|
||||
loadedFacadePluginIds.add(
|
||||
typeof params.trackedPluginId === "function"
|
||||
@@ -217,7 +281,7 @@ export function loadFacadeModuleAtLocationSync<T extends object>(params: {
|
||||
: params.trackedPluginId,
|
||||
);
|
||||
} catch (err) {
|
||||
loadedFacadeModules.delete(params.location.modulePath);
|
||||
loadedFacadeModules.delete(location.modulePath);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -240,6 +304,10 @@ export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(param
|
||||
return loadFacadeModuleAtLocationSync({
|
||||
location,
|
||||
trackedPluginId: params.trackedPluginId ?? params.dirName,
|
||||
runtimeDeps: {
|
||||
pluginId: params.dirName,
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -255,28 +323,37 @@ export async function loadBundledPluginPublicSurfaceModule<T extends object>(par
|
||||
`Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
|
||||
);
|
||||
}
|
||||
const cached = loadedFacadeModules.get(location.modulePath);
|
||||
const preparedLocation = prepareFacadeLocationForBundledRuntimeDeps({
|
||||
location,
|
||||
runtimeDeps: {
|
||||
pluginId: params.dirName,
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
},
|
||||
});
|
||||
const cached = loadedFacadeModules.get(preparedLocation.modulePath);
|
||||
if (cached) {
|
||||
return cached as T;
|
||||
}
|
||||
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
absolutePath: preparedLocation.modulePath,
|
||||
rootPath: preparedLocation.boundaryRoot,
|
||||
boundaryLabel:
|
||||
location.boundaryRoot === getOpenClawPackageRoot() ? "OpenClaw package root" : "plugin root",
|
||||
preparedLocation.boundaryRoot === getOpenClawPackageRoot()
|
||||
? "OpenClaw package root"
|
||||
: "plugin root",
|
||||
rejectHardlinks: false,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error(`Unable to open bundled plugin public surface ${location.modulePath}`, {
|
||||
throw new Error(`Unable to open bundled plugin public surface ${preparedLocation.modulePath}`, {
|
||||
cause: opened.error,
|
||||
});
|
||||
}
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
try {
|
||||
const loaded = (await import(pathToFileURL(location.modulePath).href)) as T;
|
||||
loadedFacadeModules.set(location.modulePath, loaded);
|
||||
const loaded = (await import(pathToFileURL(preparedLocation.modulePath).href)) as T;
|
||||
loadedFacadeModules.set(preparedLocation.modulePath, loaded);
|
||||
loadedFacadePluginIds.add(
|
||||
typeof params.trackedPluginId === "function"
|
||||
? params.trackedPluginId()
|
||||
@@ -285,7 +362,7 @@ export async function loadBundledPluginPublicSurfaceModule<T extends object>(par
|
||||
return loaded;
|
||||
} catch {
|
||||
return loadFacadeModuleAtLocationSync({
|
||||
location,
|
||||
location: preparedLocation,
|
||||
trackedPluginId: params.trackedPluginId ?? params.dirName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,6 +172,10 @@ function loadFacadeActivationCheckRuntime(): FacadeActivationCheckRuntimeModule
|
||||
function loadFacadeModuleAtLocationSync<T extends object>(params: {
|
||||
location: FacadeModuleLocation;
|
||||
trackedPluginId: string | (() => string);
|
||||
runtimeDeps?: {
|
||||
pluginId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
loadModule?: (modulePath: string) => T;
|
||||
}): T {
|
||||
return loadFacadeModuleAtLocationSyncShared(params);
|
||||
@@ -207,6 +211,10 @@ export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(
|
||||
return loadFacadeModuleAtLocationSync<T>({
|
||||
location,
|
||||
trackedPluginId,
|
||||
runtimeDeps: {
|
||||
pluginId: params.dirName,
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user