Compare commits

...

1 Commits

Author SHA1 Message Date
Peter Steinberger
c5f20ef579 fix(plugins): preserve staged runtime require exports 2026-04-30 03:51:19 +01:00
4 changed files with 143 additions and 8 deletions

View File

@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
- Bonjour/Gateway: cap flapping advertiser restarts in a sliding window, so mDNS probing/name-conflict loops disable discovery instead of churning indefinitely on constrained hosts. Refs #74209 and #74242. Thanks @ndj888 and @Sanjays2402.
- Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete `ajv`/MCP SDK installs after update instead of failing after restart on a missing `ajv/dist/ajv.js`. Refs #74630. Thanks @spickeringlr.
- Heartbeat: resolve `responsePrefix` template variables with the selected provider, model, and thinking context before delivering alerts or suppressing prefixed `HEARTBEAT_OK` replies. Fixes #43064; repairs #43065; supersedes #46858. Thanks @yweiii and @JunJD.
- Plugins/runtime-deps: resolve bundled runtime dependency aliases with Jiti's sync require conditions so custom plugins using `require("ws")` receive the CommonJS constructor instead of the ESM namespace wrapper. Fixes #74547. Thanks @aderius.
- Channels/Feishu: retry file-typed iOS video resource downloads as `media` after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong.
- Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (`xhigh`, `adaptive`, and `max`) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so `/think` menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys.
- Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc.

View File

@@ -42,8 +42,10 @@ describe("bundled runtime dependency Jiti aliases", () => {
const rootDir = makeTempRoot();
writeJson(path.join(rootDir, "package.json"), {
dependencies: {
"esm-only": "1.0.0",
plain: "1.0.0",
wild: "1.0.0",
ws: "8.20.0",
"@scope/pkg": "1.0.0",
},
});
@@ -56,8 +58,27 @@ describe("bundled runtime dependency Jiti aliases", () => {
},
});
writeFile(path.join(plainRoot, "esm/index.js"));
writeFile(path.join(plainRoot, "cjs/index.js"));
writeFile(path.join(plainRoot, "features/feature.js"));
const wsRoot = packageRoot(rootDir, "ws");
writeJson(path.join(wsRoot, "package.json"), {
exports: {
".": { browser: "./browser.js", import: "./wrapper.mjs", require: "./index.js" },
},
});
writeFile(path.join(wsRoot, "browser.js"));
writeFile(path.join(wsRoot, "wrapper.mjs"));
writeFile(path.join(wsRoot, "index.js"));
const esmOnlyRoot = packageRoot(rootDir, "esm-only");
writeJson(path.join(esmOnlyRoot, "package.json"), {
exports: {
".": { import: "./dist/index.mjs" },
},
});
writeFile(path.join(esmOnlyRoot, "dist/index.mjs"));
const wildRoot = packageRoot(rootDir, "wild");
writeJson(path.join(wildRoot, "package.json"), {
exports: {
@@ -80,7 +101,9 @@ describe("bundled runtime dependency Jiti aliases", () => {
"plain/feature": path.join(plainRoot, "features/feature.js"),
"@scope/pkg": path.join(scopedRoot, "index.mjs"),
"wild/sub/a": path.join(wildRoot, "dist/a.js"),
plain: path.join(plainRoot, "esm/index.js"),
"esm-only": path.join(esmOnlyRoot, "dist/index.mjs"),
plain: path.join(plainRoot, "cjs/index.js"),
ws: path.join(wsRoot, "index.js"),
});
});

View File

@@ -32,13 +32,13 @@ function collectRuntimeDependencyNames(pkg: RuntimeDependencyPackageJson): strin
].toSorted((left, right) => left.localeCompare(right));
}
function resolveRuntimePackageImportTarget(exportsField: unknown): string | null {
function resolveRuntimePackageJitiRequireTarget(exportsField: unknown): string | null {
if (typeof exportsField === "string") {
return exportsField;
}
if (Array.isArray(exportsField)) {
for (const entry of exportsField) {
const resolved = resolveRuntimePackageImportTarget(entry);
const resolved = resolveRuntimePackageJitiRequireTarget(entry);
if (resolved) {
return resolved;
}
@@ -50,10 +50,26 @@ function resolveRuntimePackageImportTarget(exportsField: unknown): string | null
}
const record = exportsField as Record<string, unknown>;
if (Object.prototype.hasOwnProperty.call(record, ".")) {
return resolveRuntimePackageImportTarget(record["."]);
return resolveRuntimePackageJitiRequireTarget(record["."]);
}
for (const condition of ["import", "node", "default"] as const) {
const resolved = resolveRuntimePackageImportTarget(record[condition]);
for (const conditions of [new Set(["node", "require"]), new Set(["node", "import"])] as const) {
const resolved = resolveRuntimePackageConditionalTarget(record, conditions);
if (resolved) {
return resolved;
}
}
return null;
}
function resolveRuntimePackageConditionalTarget(
record: Record<string, unknown>,
conditions: ReadonlySet<string>,
): string | null {
for (const [condition, target] of Object.entries(record)) {
if (condition !== "default" && !conditions.has(condition)) {
continue;
}
const resolved = resolveRuntimePackageJitiRequireTarget(target);
if (resolved) {
return resolved;
}
@@ -133,7 +149,7 @@ function collectRuntimePackageImportTargets(
if (!exportKey.startsWith(".")) {
continue;
}
const resolved = resolveRuntimePackageImportTarget(exportValue);
const resolved = resolveRuntimePackageJitiRequireTarget(exportValue);
if (resolved) {
if (exportKey.includes("*")) {
for (const [wildcardExportKey, targetPath] of collectRuntimePackageWildcardImportTargets(
@@ -150,7 +166,7 @@ function collectRuntimePackageImportTargets(
}
return targets;
}
const rootEntry = resolveRuntimePackageImportTarget(exportsField) ?? pkg.module ?? pkg.main;
const rootEntry = resolveRuntimePackageJitiRequireTarget(exportsField) ?? pkg.module ?? pkg.main;
if (rootEntry) {
targets.set(".", rootEntry);
}

View File

@@ -1594,6 +1594,101 @@ module.exports = {
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
});
it("preserves CommonJS constructor semantics for staged ws runtime deps", () => {
const bundledDir = makeTempDir();
const stageDir = makeTempDir();
const plugin = writePlugin({
id: "alpha",
dir: path.join(bundledDir, "alpha"),
filename: "index.ts",
body: `
const WebSocket = require("ws");
export default {
id: "alpha",
register() {
if (typeof WebSocket !== "function") {
throw new Error(\`expected ws constructor, got \${typeof WebSocket}\`);
}
new WebSocket();
}
};
`,
});
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
fs.writeFileSync(
path.join(plugin.dir, "package.json"),
JSON.stringify(
{
name: "@openclaw/alpha",
version: "1.0.0",
dependencies: {
ws: "8.20.0",
},
openclaw: { extensions: ["./index.ts"] },
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(plugin.dir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "alpha",
enabledByDefault: true,
configSchema: EMPTY_PLUGIN_SCHEMA,
},
null,
2,
),
"utf-8",
);
const registry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
enabled: true,
},
},
bundledRuntimeDepsInstaller: ({ installRoot }) => {
const depRoot = path.join(installRoot, "node_modules", "ws");
fs.mkdirSync(depRoot, { recursive: true });
fs.writeFileSync(
path.join(depRoot, "package.json"),
JSON.stringify({
name: "ws",
version: "8.20.0",
main: "index.cjs",
exports: {
".": {
browser: "./browser.js",
import: "./wrapper.mjs",
require: "./index.cjs",
},
},
}),
"utf-8",
);
fs.writeFileSync(path.join(depRoot, "browser.js"), "export default null;\n", "utf-8");
fs.writeFileSync(
path.join(depRoot, "wrapper.mjs"),
"function WebSocket() {}; export default WebSocket; export { WebSocket as WebSocket };\n",
"utf-8",
);
fs.writeFileSync(
path.join(depRoot, "index.cjs"),
"function WebSocket() {}; WebSocket.WebSocket = WebSocket; module.exports = WebSocket;\n",
"utf-8",
);
},
});
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
});
it("loads bundled plugins from symlinked package roots with an external stage dir", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();