mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: normalize lazy service override imports
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.
|
||||
- Plugins/install: treat mirrored core logger dependencies as staged bundled runtime deps so packaged Gateway starts do not crash when the external plugin-runtime-deps root is missing `tslog`. Fixes #72228; supersedes #72493. Thanks @deepujain.
|
||||
- Plugins/install: hide bundled runtime-dependency npm child windows on Windows across Gateway startup, postinstall, and packaged staging paths so Telegram/Anthropic dependency repair no longer flashes shell windows. Fixes #72315. Thanks @athuljayaram and @joshfeng.
|
||||
- Plugins/Windows: normalize lazy plugin service override imports before Node ESM loading so drive-letter browser-control module paths no longer fail with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. Fixes #72573; supersedes #72599 and #72582. Thanks @llzzww316, @feineryonah-byte, and @WuKongAI-CMU.
|
||||
- Plugins/install: stage bundled plugin runtime dependencies before Gateway startup, drain update restarts, and materialize plugin-owned root chunks in external mirrors so staged deps resolve under native ESM. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss.
|
||||
- TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.
|
||||
- Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the machine user scope when `systemctl --user` reports no-medium bus failures. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, and @mssteuer.
|
||||
|
||||
37
src/plugins/import-specifier.test.ts
Normal file
37
src/plugins/import-specifier.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { toSafeImportPath } from "./import-specifier.js";
|
||||
|
||||
describe("toSafeImportPath", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("converts Windows absolute import specifiers to file URLs", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
|
||||
expect(toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe(
|
||||
"file:///C:/Users/alice/plugin/index.mjs",
|
||||
);
|
||||
expect(toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe(
|
||||
"file://server/share/plugin/index.mjs",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves import specifiers that Node can already resolve", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
|
||||
expect(toSafeImportPath("file:///C:/Users/alice/plugin/index.mjs")).toBe(
|
||||
"file:///C:/Users/alice/plugin/index.mjs",
|
||||
);
|
||||
expect(toSafeImportPath("./relative/index.mjs")).toBe("./relative/index.mjs");
|
||||
expect(toSafeImportPath("@openclaw/plugin")).toBe("@openclaw/plugin");
|
||||
});
|
||||
|
||||
it("does not rewrite non-Windows paths", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
||||
|
||||
expect(toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe(
|
||||
"C:\\Users\\alice\\plugin\\index.mjs",
|
||||
);
|
||||
});
|
||||
});
|
||||
22
src/plugins/import-specifier.ts
Normal file
22
src/plugins/import-specifier.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* On Windows, Node's ESM loader requires absolute paths to be expressed as
|
||||
* file:// URLs. Raw drive-letter paths like C:\... are parsed as URL schemes.
|
||||
*/
|
||||
export function toSafeImportPath(specifier: string): string {
|
||||
if (process.platform !== "win32") {
|
||||
return specifier;
|
||||
}
|
||||
if (specifier.startsWith("file://")) {
|
||||
return specifier;
|
||||
}
|
||||
if (path.win32.isAbsolute(specifier)) {
|
||||
const normalizedSpecifier = specifier.replaceAll("\\", "/");
|
||||
if (normalizedSpecifier.startsWith("//")) {
|
||||
return new URL(`file:${encodeURI(normalizedSpecifier)}`).href;
|
||||
}
|
||||
return new URL(`file:///${encodeURI(normalizedSpecifier)}`).href;
|
||||
}
|
||||
return specifier;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { startLazyPluginServiceModule } from "./lazy-service-module.js";
|
||||
import { defaultLoadOverrideModule, startLazyPluginServiceModule } from "./lazy-service-module.js";
|
||||
|
||||
function createAsyncHookMock() {
|
||||
return vi.fn(async () => {});
|
||||
@@ -89,6 +89,40 @@ describe("startLazyPluginServiceModule", () => {
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute paths in the default override loader", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const start = createAsyncHookMock();
|
||||
const importModule = vi.fn(async () => ({ startOverride: start }));
|
||||
|
||||
try {
|
||||
await defaultLoadOverrideModule("C:\\Users\\alice\\browser-service.mjs", importModule);
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(importModule).toHaveBeenCalledWith("file:///C:/Users/alice/browser-service.mjs");
|
||||
});
|
||||
|
||||
it("leaves caller-supplied override loaders responsible for their own specifiers", async () => {
|
||||
process.env.OPENCLAW_LAZY_SERVICE_OVERRIDE = "C:\\Users\\alice\\browser-service.mjs";
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const start = createAsyncHookMock();
|
||||
const loadOverrideModule = vi.fn(async () => ({ startOverride: start }));
|
||||
|
||||
try {
|
||||
await expectLifecycleStarted({
|
||||
overrideEnvVar: "OPENCLAW_LAZY_SERVICE_OVERRIDE",
|
||||
loadOverrideModule,
|
||||
startExportNames: ["startOverride"],
|
||||
});
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(loadOverrideModule).toHaveBeenCalledWith("C:\\Users\\alice\\browser-service.mjs");
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("validates the override specifier before loading it", async () => {
|
||||
process.env.OPENCLAW_LAZY_SERVICE_OVERRIDE = "virtual:service";
|
||||
const loadOverrideModule = vi.fn(async () => ({ startOverride: createAsyncHookMock() }));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { toSafeImportPath } from "./import-specifier.js";
|
||||
|
||||
type LazyServiceModule = Record<string, unknown>;
|
||||
|
||||
@@ -17,6 +18,14 @@ function resolveExport<T>(mod: LazyServiceModule, names: string[]): T | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function defaultLoadOverrideModule(
|
||||
specifier: string,
|
||||
importModule: (specifier: string) => Promise<LazyServiceModule> = async (source: string) =>
|
||||
await import(source),
|
||||
): Promise<LazyServiceModule> {
|
||||
return importModule(toSafeImportPath(specifier));
|
||||
}
|
||||
|
||||
export async function startLazyPluginServiceModule(params: {
|
||||
skipEnvVar?: string;
|
||||
overrideEnvVar?: string;
|
||||
@@ -33,8 +42,7 @@ export async function startLazyPluginServiceModule(params: {
|
||||
|
||||
const overrideEnvVar = params.overrideEnvVar?.trim();
|
||||
const override = overrideEnvVar ? process.env[overrideEnvVar]?.trim() : undefined;
|
||||
const loadOverrideModule =
|
||||
params.loadOverrideModule ?? (async (specifier: string) => await import(specifier));
|
||||
const loadOverrideModule = params.loadOverrideModule ?? defaultLoadOverrideModule;
|
||||
const validatedOverride =
|
||||
override && params.validateOverrideSpecifier
|
||||
? params.validateOverrideSpecifier(override)
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
} from "./config-state.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { getGlobalHookRunner, initializeGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { toSafeImportPath } from "./import-specifier.js";
|
||||
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js";
|
||||
import {
|
||||
clearPluginInteractiveHandlers,
|
||||
@@ -429,32 +430,6 @@ function runPluginRegisterSync(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Windows, the Node.js ESM loader requires absolute paths to be expressed
|
||||
* as file:// URLs (e.g. file:///C:/Users/...). Raw drive-letter paths like
|
||||
* C:\... are rejected with ERR_UNSUPPORTED_ESM_URL_SCHEME because the loader
|
||||
* mistakes the drive letter for an unknown URL scheme.
|
||||
*
|
||||
* This helper converts Windows absolute import specifiers to file:// URLs and
|
||||
* leaves everything else unchanged.
|
||||
*/
|
||||
function toSafeImportPath(specifier: string): string {
|
||||
if (process.platform !== "win32") {
|
||||
return specifier;
|
||||
}
|
||||
if (specifier.startsWith("file://")) {
|
||||
return specifier;
|
||||
}
|
||||
if (path.win32.isAbsolute(specifier)) {
|
||||
const normalizedSpecifier = specifier.replaceAll("\\", "/");
|
||||
if (normalizedSpecifier.startsWith("//")) {
|
||||
return new URL(`file:${encodeURI(normalizedSpecifier)}`).href;
|
||||
}
|
||||
return new URL(`file:///${encodeURI(normalizedSpecifier)}`).href;
|
||||
}
|
||||
return specifier;
|
||||
}
|
||||
|
||||
type RuntimeDependencyPackageJson = {
|
||||
dependencies?: Record<string, unknown>;
|
||||
optionalDependencies?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user