diff --git a/scripts/check-plugin-sdk-exports.mjs b/scripts/check-plugin-sdk-exports.mjs index 60c89056ca0..90d784235f5 100755 --- a/scripts/check-plugin-sdk-exports.mjs +++ b/scripts/check-plugin-sdk-exports.mjs @@ -42,7 +42,7 @@ const exportedNames = exportMatch[1] const exportSet = new Set(exportedNames); -const requiredRuntimeShimEntries = ["root-alias.cjs"]; +const requiredRuntimeShimEntries = ["compat.js", "root-alias.cjs"]; // Critical functions that channel extension plugins import from openclaw/plugin-sdk. // If any of these are missing, plugins will fail at runtime with: @@ -65,6 +65,7 @@ const requiredExports = [ "resolveChannelMediaMaxBytes", "warnMissingProviderGroupPolicyFallbackOnce", "emptyPluginConfigSchema", + "onDiagnosticEvent", "normalizePluginHttpPath", "registerPluginHttpRoute", "DEFAULT_ACCOUNT_ID", diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 72d729cc1cd..f7f36373a49 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -21,6 +21,7 @@ const requiredPathGroups = [ ["dist/index.js", "dist/index.mjs"], ["dist/entry.js", "dist/entry.mjs"], ...listPluginSdkDistArtifacts(), + "dist/plugin-sdk/compat.js", "dist/plugin-sdk/root-alias.cjs", "dist/build-info.json", ]; @@ -228,6 +229,7 @@ const requiredPluginSdkExports = [ "resolveChannelMediaMaxBytes", "warnMissingProviderGroupPolicyFallbackOnce", "emptyPluginConfigSchema", + "onDiagnosticEvent", "normalizePluginHttpPath", "registerPluginHttpRoute", "DEFAULT_ACCOUNT_ID", diff --git a/src/infra/tsdown-config.test.ts b/src/infra/tsdown-config.test.ts index 94332c5b307..c47bbcb2192 100644 --- a/src/infra/tsdown-config.test.ts +++ b/src/infra/tsdown-config.test.ts @@ -36,6 +36,7 @@ describe("tsdown config", () => { expect.arrayContaining([ "index", "plugins/runtime/index", + "plugin-sdk/compat", "plugin-sdk/index", "extensions/openai/index", "bundled/boot-md/handler", diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index 5e2bcd11f58..99e2066633c 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -20,6 +20,8 @@ if (shouldWarnCompatImport) { export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; export { resolveControlCommandGate } from "../channels/command-gating.js"; export { delegateCompactionToRuntime } from "../context-engine/delegate.js"; +export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js"; +export { onDiagnosticEvent } from "../infra/diagnostic-events.js"; export { createAccountStatusSink } from "./channel-lifecycle.js"; export { createPluginRuntimeStore } from "./runtime-store.js"; diff --git a/src/plugin-sdk/index.test.ts b/src/plugin-sdk/index.test.ts index 30040416729..db54ebbd1ff 100644 --- a/src/plugin-sdk/index.test.ts +++ b/src/plugin-sdk/index.test.ts @@ -50,9 +50,11 @@ describe("plugin-sdk exports", () => { it("keeps the root runtime surface intentionally small", () => { expect(typeof sdk.emptyPluginConfigSchema).toBe("function"); expect(typeof sdk.delegateCompactionToRuntime).toBe("function"); + expect(typeof sdk.onDiagnosticEvent).toBe("function"); expect(Object.prototype.hasOwnProperty.call(sdk, "resolveControlCommandGate")).toBe(false); expect(Object.prototype.hasOwnProperty.call(sdk, "buildAgentSessionKey")).toBe(false); expect(Object.prototype.hasOwnProperty.call(sdk, "isDangerousNameMatchingEnabled")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(sdk, "emitDiagnosticEvent")).toBe(false); }); it("keeps package.json plugin-sdk exports synced with the manifest", async () => { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 5bb67920734..20f8a34672a 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -64,7 +64,9 @@ export type { HookEntry } from "../hooks/types.js"; export type { ReplyPayload } from "../auto-reply/types.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export type { ContextEngineFactory } from "../context-engine/registry.js"; +export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; export { registerContextEngine } from "../context-engine/registry.js"; export { delegateCompactionToRuntime } from "../context-engine/delegate.js"; +export { onDiagnosticEvent } from "../infra/diagnostic-events.js"; diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 23e583f8c4d..669586bb80c 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -5,6 +5,7 @@ const fs = require("node:fs"); let monolithicSdk = null; const jitiLoaders = new Map(); +const pluginSdkSubpathsCache = new Map(); function emptyPluginConfigSchema() { function error(message) { @@ -61,6 +62,49 @@ function resolveControlCommandGate(params) { return { commandAuthorized, shouldBlock }; } +function getPackageRoot() { + return path.resolve(__dirname, "..", ".."); +} + +function listPluginSdkExportedSubpaths() { + const packageRoot = getPackageRoot(); + if (pluginSdkSubpathsCache.has(packageRoot)) { + return pluginSdkSubpathsCache.get(packageRoot); + } + + let subpaths = []; + try { + const packageJsonPath = path.join(packageRoot, "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + subpaths = Object.keys(packageJson.exports ?? {}) + .filter((key) => key.startsWith("./plugin-sdk/")) + .map((key) => key.slice("./plugin-sdk/".length)); + } catch { + subpaths = []; + } + + pluginSdkSubpathsCache.set(packageRoot, subpaths); + return subpaths; +} + +function buildPluginSdkAliasMap(useDist) { + const packageRoot = getPackageRoot(); + const pluginSdkDir = path.join(packageRoot, useDist ? "dist" : "src", "plugin-sdk"); + const ext = useDist ? ".js" : ".ts"; + const aliasMap = { + "openclaw/plugin-sdk": __filename, + }; + + for (const subpath of listPluginSdkExportedSubpaths()) { + const candidate = path.join(pluginSdkDir, `${subpath}${ext}`); + if (fs.existsSync(candidate)) { + aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate; + } + } + + return aliasMap; +} + function getJiti(tryNative) { if (jitiLoaders.has(tryNative)) { return jitiLoaders.get(tryNative); @@ -68,6 +112,7 @@ function getJiti(tryNative) { const { createJiti } = require("jiti"); const jitiLoader = createJiti(__filename, { + alias: buildPluginSdkAliasMap(tryNative), interopDefault: true, // Prefer Node's native sync ESM loader for built dist/plugin-sdk/*.js files // so local plugins do not create a second transpiled OpenClaw core graph. diff --git a/src/plugin-sdk/root-alias.test.ts b/src/plugin-sdk/root-alias.test.ts index 83937c34b44..48ae4a7b43c 100644 --- a/src/plugin-sdk/root-alias.test.ts +++ b/src/plugin-sdk/root-alias.test.ts @@ -48,6 +48,12 @@ function loadRootAliasWithStubs(options?: { } if (id === "node:fs") { return { + readFileSync: () => + JSON.stringify({ + exports: { + "./plugin-sdk/group-access": { default: "./dist/plugin-sdk/group-access.js" }, + }, + }), existsSync: () => options?.distExists ?? false, }; } @@ -164,8 +170,23 @@ describe("plugin-sdk root alias", () => { expect("delegateCompactionToRuntime" in lazyRootSdk).toBe(true); }); + it("forwards onDiagnosticEvent through the compat-backed root alias", () => { + const onDiagnosticEvent = () => () => undefined; + const lazyModule = loadRootAliasWithStubs({ + monolithicExports: { + onDiagnosticEvent, + }, + }); + const lazyRootSdk = lazyModule.moduleExports; + + expect(typeof lazyRootSdk.onDiagnosticEvent).toBe("function"); + expect(lazyRootSdk.onDiagnosticEvent).toBe(onDiagnosticEvent); + expect("onDiagnosticEvent" in lazyRootSdk).toBe(true); + }); + it("loads legacy root exports through the merged root wrapper", { timeout: 240_000 }, () => { expect(typeof rootSdk.resolveControlCommandGate).toBe("function"); + expect(typeof rootSdk.onDiagnosticEvent).toBe("function"); expect(typeof rootSdk.default).toBe("object"); expect(rootSdk.default).toBe(rootSdk); expect(rootSdk.__esModule).toBe(true); @@ -173,9 +194,12 @@ describe("plugin-sdk root alias", () => { it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => { expect("resolveControlCommandGate" in rootSdk).toBe(true); + expect("onDiagnosticEvent" in rootSdk).toBe(true); const keys = Object.keys(rootSdk); expect(keys).toContain("resolveControlCommandGate"); + expect(keys).toContain("onDiagnosticEvent"); const descriptor = Object.getOwnPropertyDescriptor(rootSdk, "resolveControlCommandGate"); expect(descriptor).toBeDefined(); + expect(Object.getOwnPropertyDescriptor(rootSdk, "onDiagnosticEvent")).toBeDefined(); }); }); diff --git a/tsdown.config.ts b/tsdown.config.ts index 746c6e883bc..98dd9e3d341 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -186,6 +186,8 @@ const coreDistEntries = buildCoreDistEntries(); function buildUnifiedDistEntries(): Record { return { ...coreDistEntries, + // Internal compat artifact for the root-alias.cjs lazy loader. + "plugin-sdk/compat": "src/plugin-sdk/compat.ts", ...Object.fromEntries( Object.entries(buildPluginSdkEntrySources()).map(([entry, source]) => [ `plugin-sdk/${entry}`,