fix(status): resolve packaged channel setup loader

This commit is contained in:
Peter Steinberger
2026-04-30 01:57:53 +01:00
parent 80ec402d0f
commit 6863694a22
4 changed files with 59 additions and 6 deletions

View File

@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.
- Channels/Microsoft Teams: treat configured `19:...@thread.tacv2` and legacy `19:...@thread.skype` team/channel IDs as already resolved during startup, avoiding false `channels unresolved` warnings while preserving Graph name lookup for display-name entries. Fixes #74683. Thanks @dseravalli.

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import {
cleanupPluginLoaderFixturesForTest,
@@ -8,7 +9,10 @@ import {
resetPluginLoaderTestStateForTest,
useNoBundledPlugins,
} from "../../plugins/loader.test-fixtures.js";
import { listReadOnlyChannelPluginsForConfig } from "./read-only.js";
import {
listPluginLoaderModuleCandidateUrls,
listReadOnlyChannelPluginsForConfig,
} from "./read-only.js";
vi.mock("../../plugins/bundled-dir.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../plugins/bundled-dir.js")>();
@@ -423,6 +427,20 @@ afterAll(() => {
});
describe("listReadOnlyChannelPluginsForConfig", () => {
it("keeps built plugin loader candidates inside the installed package dist root", () => {
const packageRoot = path.join(makeTempDir(), "node_modules", "openclaw");
const importerPath = path.join(packageRoot, "dist", "read-only-B4EkEtUx.js");
const candidates = listPluginLoaderModuleCandidateUrls(pathToFileURL(importerPath).href).map(
(candidate) => fileURLToPath(candidate),
);
expect(candidates).toEqual([
path.join(packageRoot, "dist", "plugins", "loader.js"),
path.join(packageRoot, "dist", "plugins", "build-smoke-entry.js"),
]);
expect(candidates).not.toContain(path.join(packageRoot, "..", "plugins", "loader.js"));
});
it("does not load setup-only channel plugin runtime by default", () => {
const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin();
const plugins = listReadOnlyChannelPluginsForConfig(

View File

@@ -1,4 +1,5 @@
import { fileURLToPath } from "node:url";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
@@ -25,9 +26,13 @@ import {
import { listChannelPlugins } from "./registry.js";
import type { ChannelPlugin } from "./types.plugin.js";
const LOADER_MODULE_CANDIDATES = [
new URL("../../plugins/loader.js", import.meta.url),
new URL("../../plugins/loader.ts", import.meta.url),
const SOURCE_PLUGIN_LOADER_MODULE_CANDIDATES = [
"../../plugins/loader.js",
"../../plugins/loader.ts",
] as const;
const BUILT_PLUGIN_LOADER_MODULE_CANDIDATES = [
"plugins/loader.js",
"plugins/build-smoke-entry.js",
] as const;
const jitiLoaders: PluginJitiLoaderCache = new Map();
@@ -53,11 +58,39 @@ type PluginLoaderModule = {
let pluginLoaderModule: PluginLoaderModule | undefined;
function listBuiltPluginLoaderModuleCandidateUrls(importerUrl: string): URL[] {
let importerPath: string;
try {
importerPath = fileURLToPath(importerUrl);
} catch {
return [];
}
const distMarker = `${path.sep}dist${path.sep}`;
const distMarkerIndex = importerPath.lastIndexOf(distMarker);
if (distMarkerIndex < 0) {
return [];
}
// Bundled read-only chunks live under dist/ with hashed names. Source-relative
// ../../plugins candidates would escape the installed openclaw package there.
const distRoot = importerPath.slice(0, distMarkerIndex + distMarker.length - 1);
return BUILT_PLUGIN_LOADER_MODULE_CANDIDATES.map((candidate) =>
pathToFileURL(path.join(distRoot, candidate)),
);
}
export function listPluginLoaderModuleCandidateUrls(importerUrl = import.meta.url): URL[] {
const builtCandidates = listBuiltPluginLoaderModuleCandidateUrls(importerUrl);
if (builtCandidates.length > 0) {
return builtCandidates;
}
return SOURCE_PLUGIN_LOADER_MODULE_CANDIDATES.map((candidate) => new URL(candidate, importerUrl));
}
function loadPluginLoaderModule(): PluginLoaderModule {
if (pluginLoaderModule) {
return pluginLoaderModule;
}
for (const candidate of LOADER_MODULE_CANDIDATES) {
for (const candidate of listPluginLoaderModuleCandidateUrls()) {
const modulePath = fileURLToPath(candidate);
try {
const jiti = getCachedPluginJitiLoader({

View File

@@ -226,6 +226,7 @@ function buildCoreDistEntries(): Record<string, string> {
"plugins/provider-discovery.runtime": "src/plugins/provider-discovery.runtime.ts",
"plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts",
"plugins/public-surface-runtime": "src/plugins/public-surface-runtime.ts",
"plugins/loader": "src/plugins/loader.ts",
"plugins/sdk-alias": "src/plugins/sdk-alias.ts",
"facade-activation-check.runtime": "src/plugin-sdk/facade-activation-check.runtime.ts",
extensionAPI: "src/extensionAPI.ts",