From 25d0aa7296031f7f60d3024fec0bc6b725a3330c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 17:08:33 +0000 Subject: [PATCH] refactor: simplify plugin sdk compatibility aliases --- src/plugin-sdk/root-alias.cjs | 106 +++++------------------- src/plugin-sdk/root-alias.test.ts | 2 +- src/plugins/loader.test.ts | 7 ++ src/plugins/loader.ts | 129 +++++++++--------------------- 4 files changed, 64 insertions(+), 180 deletions(-) diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index aa2127bdc..9e3c2e5d1 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -108,92 +108,26 @@ const fastExports = { resolveControlCommandGate, }; -const rootProxy = new Proxy(fastExports, { - get(target, prop, receiver) { - if (prop === "__esModule") { - return true; - } - if (prop === "default") { - return rootProxy; - } - if (Reflect.has(target, prop)) { - return Reflect.get(target, prop, receiver); - } - return loadMonolithicSdk()[prop]; - }, - has(target, prop) { - if (prop === "__esModule" || prop === "default") { - return true; - } - if (Reflect.has(target, prop)) { - return true; - } - const monolithic = tryLoadMonolithicSdk(); - return monolithic ? prop in monolithic : false; - }, - ownKeys(target) { - const keys = new Set([...Reflect.ownKeys(target), "default", "__esModule"]); - // Keep Object.keys/property reflection fast and deterministic. - // Only expose monolithic keys if it was already loaded by direct access. - if (monolithicSdk) { - for (const key of Reflect.ownKeys(monolithicSdk)) { - keys.add(key); +const monolithic = tryLoadMonolithicSdk(); +const rootExports = + monolithic && typeof monolithic === "object" + ? { + ...monolithic, + ...fastExports, } - } - return [...keys]; - }, - getOwnPropertyDescriptor(target, prop) { - if (prop === "__esModule") { - return { - configurable: true, - enumerable: false, - writable: false, - value: true, - }; - } - if (prop === "default") { - return { - configurable: true, - enumerable: false, - writable: false, - value: rootProxy, - }; - } - const own = Object.getOwnPropertyDescriptor(target, prop); - if (own) { - return own; - } - const monolithic = tryLoadMonolithicSdk(); - if (!monolithic) { - return undefined; - } - const descriptor = Object.getOwnPropertyDescriptor(monolithic, prop); - if (!descriptor) { - return undefined; - } - if (descriptor.get || descriptor.set) { - return { - configurable: true, - enumerable: descriptor.enumerable ?? true, - get: descriptor.get - ? function getLegacyValue() { - return descriptor.get.call(monolithic); - } - : undefined, - set: descriptor.set - ? function setLegacyValue(value) { - return descriptor.set.call(monolithic, value); - } - : undefined, - }; - } - return { - configurable: true, - enumerable: descriptor.enumerable ?? true, - value: descriptor.value, - writable: descriptor.writable, - }; - }, + : { ...fastExports }; + +Object.defineProperty(rootExports, "__esModule", { + configurable: true, + enumerable: false, + writable: false, + value: true, +}); +Object.defineProperty(rootExports, "default", { + configurable: true, + enumerable: false, + writable: false, + value: rootExports, }); -module.exports = rootProxy; +module.exports = rootExports; diff --git a/src/plugin-sdk/root-alias.test.ts b/src/plugin-sdk/root-alias.test.ts index 6cffdd3c9..8757d3ce3 100644 --- a/src/plugin-sdk/root-alias.test.ts +++ b/src/plugin-sdk/root-alias.test.ts @@ -27,7 +27,7 @@ describe("plugin-sdk root alias", () => { expect(parsed.success).toBe(false); }); - it("loads legacy root exports lazily through the proxy", { timeout: 240_000 }, () => { + it("loads legacy root exports through the merged root wrapper", { timeout: 240_000 }, () => { expect(typeof rootSdk.resolveControlCommandGate).toBe("function"); expect(typeof rootSdk.default).toBe("object"); expect(rootSdk.default).toBe(rootSdk); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 58f44bce9..cff49aa8a 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1439,6 +1439,13 @@ describe("loadOpenClawPlugins", () => { expect(candidates.indexOf(srcFile)).toBeLessThan(candidates.indexOf(distFile)); }); + it("derives plugin-sdk subpaths from package exports", () => { + const subpaths = __testing.listPluginSdkExportedSubpaths(); + expect(subpaths).toContain("compat"); + expect(subpaths).toContain("telegram"); + expect(subpaths).not.toContain("root-alias"); + }); + it("falls back to src plugin-sdk alias when dist is missing in production", () => { const { root, srcFile, distFile } = createPluginSdkAliasFixture(); fs.rmSync(distFile); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 073e735a7..41a2f0fa3 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -5,6 +5,7 @@ import { createJiti } from "jiti"; import type { OpenClawConfig } from "../config/config.js"; import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; +import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveUserPath } from "../utils.js"; import { clearPluginCommands } from "./commands.js"; @@ -111,105 +112,46 @@ const resolvePluginSdkAliasFile = (params: { const resolvePluginSdkAlias = (): string | null => resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" }); -const pluginSdkScopedAliasEntries = [ - { subpath: "core", srcFile: "core.ts", distFile: "core.js" }, - { subpath: "compat", srcFile: "compat.ts", distFile: "compat.js" }, - { subpath: "telegram", srcFile: "telegram.ts", distFile: "telegram.js" }, - { subpath: "discord", srcFile: "discord.ts", distFile: "discord.js" }, - { subpath: "slack", srcFile: "slack.ts", distFile: "slack.js" }, - { subpath: "signal", srcFile: "signal.ts", distFile: "signal.js" }, - { subpath: "imessage", srcFile: "imessage.ts", distFile: "imessage.js" }, - { subpath: "whatsapp", srcFile: "whatsapp.ts", distFile: "whatsapp.js" }, - { subpath: "line", srcFile: "line.ts", distFile: "line.js" }, - { subpath: "msteams", srcFile: "msteams.ts", distFile: "msteams.js" }, - { subpath: "acpx", srcFile: "acpx.ts", distFile: "acpx.js" }, - { subpath: "bluebubbles", srcFile: "bluebubbles.ts", distFile: "bluebubbles.js" }, - { - subpath: "copilot-proxy", - srcFile: "copilot-proxy.ts", - distFile: "copilot-proxy.js", - }, - { subpath: "device-pair", srcFile: "device-pair.ts", distFile: "device-pair.js" }, - { - subpath: "diagnostics-otel", - srcFile: "diagnostics-otel.ts", - distFile: "diagnostics-otel.js", - }, - { subpath: "diffs", srcFile: "diffs.ts", distFile: "diffs.js" }, - { subpath: "feishu", srcFile: "feishu.ts", distFile: "feishu.js" }, - { - subpath: "google-gemini-cli-auth", - srcFile: "google-gemini-cli-auth.ts", - distFile: "google-gemini-cli-auth.js", - }, - { subpath: "googlechat", srcFile: "googlechat.ts", distFile: "googlechat.js" }, - { subpath: "irc", srcFile: "irc.ts", distFile: "irc.js" }, - { subpath: "llm-task", srcFile: "llm-task.ts", distFile: "llm-task.js" }, - { subpath: "lobster", srcFile: "lobster.ts", distFile: "lobster.js" }, - { subpath: "matrix", srcFile: "matrix.ts", distFile: "matrix.js" }, - { subpath: "mattermost", srcFile: "mattermost.ts", distFile: "mattermost.js" }, - { subpath: "memory-core", srcFile: "memory-core.ts", distFile: "memory-core.js" }, - { - subpath: "memory-lancedb", - srcFile: "memory-lancedb.ts", - distFile: "memory-lancedb.js", - }, - { - subpath: "minimax-portal-auth", - srcFile: "minimax-portal-auth.ts", - distFile: "minimax-portal-auth.js", - }, - { - subpath: "nextcloud-talk", - srcFile: "nextcloud-talk.ts", - distFile: "nextcloud-talk.js", - }, - { subpath: "nostr", srcFile: "nostr.ts", distFile: "nostr.js" }, - { subpath: "open-prose", srcFile: "open-prose.ts", distFile: "open-prose.js" }, - { - subpath: "phone-control", - srcFile: "phone-control.ts", - distFile: "phone-control.js", - }, - { - subpath: "qwen-portal-auth", - srcFile: "qwen-portal-auth.ts", - distFile: "qwen-portal-auth.js", - }, - { - subpath: "synology-chat", - srcFile: "synology-chat.ts", - distFile: "synology-chat.js", - }, - { subpath: "talk-voice", srcFile: "talk-voice.ts", distFile: "talk-voice.js" }, - { subpath: "test-utils", srcFile: "test-utils.ts", distFile: "test-utils.js" }, - { - subpath: "thread-ownership", - srcFile: "thread-ownership.ts", - distFile: "thread-ownership.js", - }, - { subpath: "tlon", srcFile: "tlon.ts", distFile: "tlon.js" }, - { subpath: "twitch", srcFile: "twitch.ts", distFile: "twitch.js" }, - { subpath: "voice-call", srcFile: "voice-call.ts", distFile: "voice-call.js" }, - { subpath: "zalo", srcFile: "zalo.ts", distFile: "zalo.js" }, - { subpath: "zalouser", srcFile: "zalouser.ts", distFile: "zalouser.js" }, - { subpath: "account-id", srcFile: "account-id.ts", distFile: "account-id.js" }, - { - subpath: "keyed-async-queue", - srcFile: "keyed-async-queue.ts", - distFile: "keyed-async-queue.js", - }, -] as const; +const cachedPluginSdkExportedSubpaths = new Map(); + +function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] { + const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); + const packageRoot = resolveOpenClawPackageRootSync({ + cwd: path.dirname(modulePath), + }); + if (!packageRoot) { + return []; + } + const cached = cachedPluginSdkExportedSubpaths.get(packageRoot); + if (cached) { + return cached; + } + try { + const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"); + const pkg = JSON.parse(pkgRaw) as { + exports?: Record; + }; + const subpaths = Object.keys(pkg.exports ?? {}) + .filter((key) => key.startsWith("./plugin-sdk/")) + .map((key) => key.slice("./plugin-sdk/".length)) + .filter((subpath) => Boolean(subpath) && !subpath.includes("/")) + .toSorted(); + cachedPluginSdkExportedSubpaths.set(packageRoot, subpaths); + return subpaths; + } catch { + return []; + } +} const resolvePluginSdkScopedAliasMap = (): Record => { const aliasMap: Record = {}; - for (const entry of pluginSdkScopedAliasEntries) { + for (const subpath of listPluginSdkExportedSubpaths()) { const resolved = resolvePluginSdkAliasFile({ - srcFile: entry.srcFile, - distFile: entry.distFile, + srcFile: `${subpath}.ts`, + distFile: `${subpath}.js`, }); if (resolved) { - aliasMap[`openclaw/plugin-sdk/${entry.subpath}`] = resolved; + aliasMap[`openclaw/plugin-sdk/${subpath}`] = resolved; } } return aliasMap; @@ -217,6 +159,7 @@ const resolvePluginSdkScopedAliasMap = (): Record => { export const __testing = { listPluginSdkAliasCandidates, + listPluginSdkExportedSubpaths, resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, };