From b056d594b4695198f7925ef1adfaca0dfac9b072 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 12:39:06 +0100 Subject: [PATCH] fix(plugins): normalize Windows Jiti paths --- CHANGELOG.md | 1 + src/plugins/jiti-loader-cache.test.ts | 69 +++++++++++++++++++++++++++ src/plugins/jiti-loader-cache.ts | 18 +++++-- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 393fa54d401..b4d423b7b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts index 44c5f3ada79..411958a470c 100644 --- a/src/plugins/jiti-loader-cache.test.ts +++ b/src/plugins/jiti-loader-cache.test.ts @@ -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); diff --git a/src/plugins/jiti-loader-cache.ts b/src/plugins/jiti-loader-cache.ts index 6656e6db3ec..d2fc3d6dbfd 100644 --- a/src/plugins/jiti-loader-cache.ts +++ b/src/plugins/jiti-loader-cache.ts @@ -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;