fix(plugins): normalize Windows Jiti paths

This commit is contained in:
Peter Steinberger
2026-04-27 12:39:06 +01:00
parent c85065eb7f
commit b056d594b4
3 changed files with 84 additions and 4 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/Windows: normalize Windows absolute paths before handing bundled plugin modules to Jiti, so Feishu/Lark message sending no longer fails with unsupported `c:` ESM loader URLs. Fixes #72783. Thanks @jackychen-png.
- CLI/doctor: run bundled plugin runtime-dependency repairs through the async npm installer with spinner/line progress and heartbeat updates, so long `openclaw doctor --fix` installs no longer look hung in TTY or piped output. Fixes #72775. Thanks @dfpalhano.
- Feishu/Windows: normalize bundled channel sidecar loads before Jiti evaluates them, so Feishu outbound sends no longer fail with raw `C:` ESM loader errors on Windows. Fixes #72783. Thanks @jackychen-png.
- Agents/tools: ignore volatile `exec` runtime metadata when comparing tool-loop outcomes, so enabled loop detection can stop repeated identical shell-command results instead of resetting on duration, PID, session, or cwd changes. Fixes #34574; supersedes #41502. Thanks @gucasbrg and @Zcg2021.

View File

@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../test/helpers/import-fresh.ts";
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
vi.doUnmock("jiti");
});
@@ -259,6 +260,39 @@ describe("getCachedPluginJitiLoader", () => {
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
});
it("normalizes Windows absolute paths before creating and calling jiti", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
const createJiti = vi.fn(() => jitiLoader);
vi.doMock("jiti", () => ({ createJiti }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=windows-jiti-paths");
const cache = new Map();
const loader = getCachedPluginJitiLoader({
cache,
modulePath: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
importerUrl: "file:///C:/Users/alice/openclaw/dist/src/plugins/public-surface-loader.js",
jitiFilename: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
tryNative: true,
});
loader("C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js");
expect(createJiti).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
expect.objectContaining({ tryNative: true }),
);
expect(jitiLoader).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
);
});
it("skips the native-require fast path when tryNative is explicitly false", async () => {
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
const createJiti = vi.fn(() => jitiLoader);
@@ -290,6 +324,41 @@ describe("getCachedPluginJitiLoader", () => {
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
});
it("normalizes Windows absolute paths when native loading is disabled", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
const createJiti = vi.fn(() => jitiLoader);
vi.doMock("jiti", () => ({ createJiti }));
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=windows-jiti-no-native");
const cache = new Map();
const loader = getCachedPluginJitiLoader({
cache,
modulePath: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
importerUrl: "file:///C:/Users/alice/openclaw/src/plugins/loader.ts",
jitiFilename: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
tryNative: false,
});
loader("C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts");
expect(nativeStub).not.toHaveBeenCalled();
expect(createJiti).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/extensions/feishu/api.ts",
expect.objectContaining({ tryNative: false }),
);
expect(jitiLoader).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/extensions/feishu/api.ts",
);
});
it("forwards extra loader arguments through to the jiti fallback", async () => {
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
const createJiti = vi.fn(() => jitiLoader);

View File

@@ -1,4 +1,5 @@
import { createJiti } from "jiti";
import { toSafeImportPath } from "../shared/import-specifier.js";
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
import {
buildPluginLoaderJitiOptions,
@@ -24,7 +25,7 @@ export function getCachedPluginJitiLoader(params: {
pluginSdkResolution?: PluginSdkResolutionPreference;
cacheScopeKey?: string;
}): PluginJitiLoader {
const jitiFilename = params.jitiFilename ?? params.modulePath;
const jitiFilename = toSafeImportPath(params.jitiFilename ?? params.modulePath);
if (params.cacheScopeKey) {
const scopedCacheKey = `${jitiFilename}::${params.cacheScopeKey}`;
const cached = params.cache.get(scopedCacheKey);
@@ -79,13 +80,22 @@ export function getCachedPluginJitiLoader(params: {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
const loadWithJiti = new Proxy(jitiLoader, {
apply(target, thisArg, argArray) {
const [first, ...rest] = argArray as [unknown, ...unknown[]];
if (typeof first === "string") {
return Reflect.apply(target, thisArg, [toSafeImportPath(first), ...rest] as never) as never;
}
return Reflect.apply(target, thisArg, argArray as never) as never;
},
});
// When the caller has explicitly opted out of native loading (for example
// `bundled-capability-runtime` in Vitest+dist mode, which depends on
// jiti's alias rewriting to surface a narrow SDK slice), route every
// target through jiti so those alias rewrites still apply.
if (!tryNative) {
params.cache.set(scopedCacheKey, jitiLoader);
return jitiLoader;
params.cache.set(scopedCacheKey, loadWithJiti);
return loadWithJiti;
}
// Otherwise prefer native require() for already-compiled JS artifacts
// (the bundled plugin public surfaces shipped in dist/). jiti's transform
@@ -99,7 +109,7 @@ export function getCachedPluginJitiLoader(params: {
if (native.ok) {
return native.moduleExport;
}
return (jitiLoader as (t: string, ...a: unknown[]) => unknown)(target, ...rest);
return (loadWithJiti as (t: string, ...a: unknown[]) => unknown)(target, ...rest);
}) as PluginJitiLoader;
params.cache.set(scopedCacheKey, loader);
return loader;