From 9d27d09d4762b8588f37b890db8d536a0e7aca0e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 18:31:37 +0100 Subject: [PATCH] fix: add plugin load debug shape --- docs/cli/plugins.md | 4 +++ src/plugins/loader.test.ts | 25 +++++++++++++++++++ src/plugins/loader.ts | 51 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index ebf0d3c76c6..b292be325e8 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -294,6 +294,10 @@ openclaw plugins doctor compatibility notices. When everything is clean it prints `No plugin issues detected.` +For module-shape failures such as missing `register`/`activate` exports, rerun +with `OPENCLAW_PLUGIN_LOAD_DEBUG=1` to include a compact export-shape summary in +the diagnostic output. + ### Marketplace ```bash diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index edfa4ef0753..f10018fa52a 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3531,6 +3531,31 @@ module.exports = { id: "throws-after-import", register() {} };`, ).toBe(true); }); + it("can include plugin export shape when register is missing", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "missing-register-shape", + filename: "missing-register-shape.cjs", + body: `module.exports = { default: { default: { id: "missing-register-shape" } } };`, + }); + + const registry = withEnv({ OPENCLAW_PLUGIN_LOAD_DEBUG: "1" }, () => + loadRegistryFromSinglePlugin({ + plugin, + pluginConfig: { + allow: ["missing-register-shape"], + }, + }), + ); + + const loaded = registry.plugins.find((entry) => entry.id === "missing-register-shape"); + expect(loaded?.status).toBe("error"); + expect(loaded?.error).toContain("plugin export missing register/activate"); + expect(loaded?.error).toContain("module shape:"); + expect(loaded?.error).toContain("export:object keys=default"); + expect(loaded?.error).toContain("export.default:object keys=default"); + }); + it("handles single-plugin channel, context engine, and cli validation", () => { useNoBundledPlugins(); const scenarios = [ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 5c52cdd1155..df4cf9325aa 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1022,6 +1022,53 @@ function resolvePluginModuleExport(moduleExport: unknown): { return {}; } +function isPluginLoadDebugEnabled(env: NodeJS.ProcessEnv): boolean { + const normalized = normalizeLowercaseStringOrEmpty(env.OPENCLAW_PLUGIN_LOAD_DEBUG); + return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; +} + +function describePluginModuleExportShape( + value: unknown, + label = "export", + seen: Set = new Set(), +): string[] { + if (value === null) { + return [`${label}:null`]; + } + if (typeof value !== "object") { + return [`${label}:${typeof value}`]; + } + if (seen.has(value)) { + return [`${label}:circular`]; + } + seen.add(value); + + const record = value as Record; + const keys = Object.keys(record).toSorted(); + const visibleKeys = keys.slice(0, 8); + const extraCount = keys.length - visibleKeys.length; + const keySummary = + visibleKeys.length > 0 + ? `${visibleKeys.join(",")}${extraCount > 0 ? `,+${extraCount}` : ""}` + : "none"; + const details = [`${label}:object keys=${keySummary}`]; + + for (const key of ["default", "module", "register", "activate"]) { + if (Object.prototype.hasOwnProperty.call(record, key)) { + details.push(...describePluginModuleExportShape(record[key], `${label}.${key}`, seen)); + } + } + return details; +} + +function formatMissingPluginRegisterError(moduleExport: unknown, env: NodeJS.ProcessEnv): string { + const message = "plugin export missing register/activate"; + if (!isPluginLoadDebugEnabled(env)) { + return message; + } + return `${message} (module shape: ${describePluginModuleExportShape(moduleExport).join("; ")})`; +} + function mergeChannelPluginSection( baseValue: T | undefined, overrideValue: T | undefined, @@ -2511,7 +2558,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi if (typeof register !== "function") { logger.error(`[plugins] ${record.id} missing register/activate export`); - pushPluginLoadError("plugin export missing register/activate"); + pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); continue; } @@ -2934,7 +2981,7 @@ export async function loadOpenClawPluginCliRegistry( if (typeof register !== "function") { logger.error(`[plugins] ${record.id} missing register/activate export`); - pushPluginLoadError("plugin export missing register/activate"); + pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); continue; }