From 9e34fb9febe2918f274ab7d86e12d64e4829eb51 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 28 Apr 2026 21:29:37 -0700 Subject: [PATCH] fix(plugin-sdk): restore channel compatibility facades --- CHANGELOG.md | 1 + docs/plugins/sdk-overview.md | 7 ++-- docs/plugins/sdk-subpaths.md | 3 +- package.json | 4 +++ scripts/lib/plugin-sdk-entrypoints.json | 1 + src/plugin-sdk/discord.test.ts | 43 +++++++++++++++++++++++ src/plugin-sdk/discord.ts | 25 ++++++++++++++ src/plugin-sdk/entrypoints.ts | 1 + src/plugin-sdk/telegram-account.test.ts | 46 +++++++++++++++++++++++++ src/plugin-sdk/telegram-account.ts | 26 ++++++++++++++ src/plugins/compat/registry.ts | 19 ++++++++++ 11 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/plugin-sdk/telegram-account.test.ts create mode 100644 src/plugin-sdk/telegram-account.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ae3b8ac23..8e2ac576582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugin SDK: add tracked Discord component-message helpers and a Telegram account-resolution compatibility facade, so existing plugins using those subpaths resolve while new plugins stay on generic channel SDK contracts. Thanks @vincentkoc. - Docs/Hetzner: clarify that SSH tunnel access requires `AllowTcpForwarding local` before running `ssh -L`, so hardened VPS sshd configs do not block loopback Gateway access. Fixes #54557; carries forward #54564; refs #54954. Thanks @satishkc7, @blackstrype, and @Aftabbs. - Gateway/shutdown: report structured shutdown warnings and HTTP close timeout warnings through `ShutdownResult` while preserving lifecycle hook hardening. Carries forward #41296. Thanks @edenfunf. - Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc. diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index b2c1c26e6ff..b6f2774d310 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -51,9 +51,10 @@ map when they have tracked owner usage. They exist for bundled-plugin maintenance only and are not recommended import paths for new third-party plugins. -`openclaw/plugin-sdk/discord` is also kept as a deprecated compatibility facade -for the published `@openclaw/discord@2026.3.13` package. Do not copy that import -path into new plugins; use the generic channel SDK subpaths instead. +`openclaw/plugin-sdk/discord` and `openclaw/plugin-sdk/telegram-account` are +also kept as deprecated compatibility facades for tracked owner usage. Do not +copy those import paths into new plugins; use injected runtime helpers and +generic channel SDK subpaths instead. ## Subpath reference diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 3bdb3ee2274..52ec6856b79 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -84,7 +84,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers | | `plugin-sdk/group-access` | Shared group-access decision helpers | | `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers | - | `plugin-sdk/discord` | Deprecated Discord compatibility facade for published `@openclaw/discord@2026.3.13`; new plugins should use generic channel SDK subpaths | + | `plugin-sdk/discord` | Deprecated Discord compatibility facade for published `@openclaw/discord@2026.3.13` and tracked owner compatibility; new plugins should use generic channel SDK subpaths | + | `plugin-sdk/telegram-account` | Deprecated Telegram account-resolution compatibility facade for tracked owner compatibility; new plugins should use injected runtime helpers or generic channel SDK subpaths | | `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) | | `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers | | `plugin-sdk/channel-inbound-debounce` | Narrow inbound debounce helpers | diff --git a/package.json b/package.json index 1eaa2bc7989..d12340b4b71 100644 --- a/package.json +++ b/package.json @@ -1193,6 +1193,10 @@ "types": "./dist/plugin-sdk/target-resolver-runtime.d.ts", "default": "./dist/plugin-sdk/target-resolver-runtime.js" }, + "./plugin-sdk/telegram-account": { + "types": "./dist/plugin-sdk/telegram-account.d.ts", + "default": "./dist/plugin-sdk/telegram-account.js" + }, "./plugin-sdk/telegram-command-config": { "types": "./dist/plugin-sdk/telegram-command-config.d.ts", "default": "./dist/plugin-sdk/telegram-command-config.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 485d442c4ef..d74d44d3c3b 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -281,6 +281,7 @@ "string-normalization-runtime", "state-paths", "target-resolver-runtime", + "telegram-account", "telegram-command-config", "text-autolink-runtime", "tool-payload", diff --git a/src/plugin-sdk/discord.test.ts b/src/plugin-sdk/discord.test.ts index 85e9f482ea2..a5d4bf6ccf8 100644 --- a/src/plugin-sdk/discord.test.ts +++ b/src/plugin-sdk/discord.test.ts @@ -3,6 +3,10 @@ import { describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => { const runtimeConfig = { channels: { discord: { token: "token" } } }; const apiModule = { + buildDiscordComponentMessage: vi.fn((params: { spec: { text?: string } }) => ({ + components: [], + text: params.spec.text ?? "", + })), collectDiscordStatusIssues: vi.fn(() => []), discordOnboardingAdapter: { kind: "legacy-onboarding" }, inspectDiscordAccount: vi.fn(() => ({ accountId: "default" })), @@ -33,7 +37,9 @@ const mocks = vi.hoisted(() => { cfg: params.cfg, })), collectDiscordAuditChannelIds: vi.fn(() => ({ channelIds: [], unresolvedChannels: [] })), + editDiscordComponentMessage: vi.fn(async () => ({ id: "message" })), listThreadBindingsBySessionKey: vi.fn(() => []), + registerBuiltDiscordComponentMessage: vi.fn(), unbindThreadBindingsBySessionKey: vi.fn(() => []), }; @@ -78,6 +84,7 @@ describe("discord plugin-sdk compatibility facade", () => { "PAIRING_APPROVED_MESSAGE", "applyAccountNameToChannelSection", "autoBindSpawnedDiscordSubagent", + "buildDiscordComponentMessage", "buildChannelConfigSchema", "buildComputedAccountStatusSnapshot", "buildTokenChannelStatusSummary", @@ -97,6 +104,8 @@ describe("discord plugin-sdk compatibility facade", () => { "normalizeDiscordMessagingTarget", "normalizeDiscordOutboundTarget", "projectCredentialSnapshotFields", + "editDiscordComponentMessage", + "registerBuiltDiscordComponentMessage", "resolveConfiguredFromCredentialStatuses", "resolveDefaultDiscordAccountId", "resolveDiscordAccount", @@ -108,6 +117,40 @@ describe("discord plugin-sdk compatibility facade", () => { } }); + it("forwards Discord component helpers through the compatibility facade", async () => { + const { + buildDiscordComponentMessage, + editDiscordComponentMessage, + registerBuiltDiscordComponentMessage, + } = await import("./discord.js"); + + const built = buildDiscordComponentMessage({ spec: { text: "hello" } }); + await editDiscordComponentMessage( + "channel", + "message", + { text: "edited" }, + { cfg: mocks.runtimeConfig }, + ); + registerBuiltDiscordComponentMessage({ + buildResult: built, + messageId: "message", + }); + + expect(mocks.apiModule.buildDiscordComponentMessage).toHaveBeenCalledWith({ + spec: { text: "hello" }, + }); + expect(mocks.runtimeModule.editDiscordComponentMessage).toHaveBeenCalledWith( + "channel", + "message", + { text: "edited" }, + { cfg: mocks.runtimeConfig }, + ); + expect(mocks.runtimeModule.registerBuiltDiscordComponentMessage).toHaveBeenCalledWith({ + buildResult: built, + messageId: "message", + }); + }); + it("keeps legacy Discord subagent auto-bind calls working without cfg", async () => { const { autoBindSpawnedDiscordSubagent } = await import("./discord.js"); diff --git a/src/plugin-sdk/discord.ts b/src/plugin-sdk/discord.ts index ffd3527962b..1f0f3f6d67c 100644 --- a/src/plugin-sdk/discord.ts +++ b/src/plugin-sdk/discord.ts @@ -12,6 +12,10 @@ import { } from "./facade-loader.js"; import { getRuntimeConfig, getRuntimeConfigSnapshot } from "./runtime-config-snapshot.js"; +export type { + DiscordComponentBuildResult, + DiscordComponentMessageSpec, +} from "../../extensions/discord/api.js"; export type { ChannelMessageActionAdapter, ChannelMessageActionName } from "./channel-contract.js"; export type { ChannelPlugin } from "./channel-core.js"; export type { OpenClawConfig } from "./config-types.js"; @@ -68,6 +72,7 @@ type DirectoryConfigParams = { type DiscordApiFacadeModule = { collectDiscordStatusIssues: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[]; + buildDiscordComponentMessage: typeof import("../../extensions/discord/api.js").buildDiscordComponentMessage; discordOnboardingAdapter?: NonNullable["setup"]>; inspectDiscordAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => unknown; listDiscordAccountIds: (cfg: OpenClawConfig) => string[]; @@ -90,6 +95,8 @@ type DiscordApiFacadeModule = { }; type DiscordRuntimeFacadeModule = { + editDiscordComponentMessage: typeof import("../../extensions/discord/runtime-api.js").editDiscordComponentMessage; + registerBuiltDiscordComponentMessage: typeof import("../../extensions/discord/runtime-api.js").registerBuiltDiscordComponentMessage; autoBindSpawnedDiscordSubagent: (params: { cfg: OpenClawConfig; accountId?: string; @@ -148,6 +155,12 @@ export function collectDiscordStatusIssues( return loadDiscordApiFacadeModule().collectDiscordStatusIssues(accounts); } +export const buildDiscordComponentMessage: DiscordApiFacadeModule["buildDiscordComponentMessage"] = + ((...args) => + loadDiscordApiFacadeModule().buildDiscordComponentMessage( + ...args, + )) as DiscordApiFacadeModule["buildDiscordComponentMessage"]; + export function inspectDiscordAccount(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -211,6 +224,18 @@ export function collectDiscordAuditChannelIds(params: { return loadDiscordRuntimeFacadeModule().collectDiscordAuditChannelIds(params); } +export const editDiscordComponentMessage: DiscordRuntimeFacadeModule["editDiscordComponentMessage"] = + ((...args) => + loadDiscordRuntimeFacadeModule().editDiscordComponentMessage( + ...args, + )) as DiscordRuntimeFacadeModule["editDiscordComponentMessage"]; + +export const registerBuiltDiscordComponentMessage: DiscordRuntimeFacadeModule["registerBuiltDiscordComponentMessage"] = + ((...args) => + loadDiscordRuntimeFacadeModule().registerBuiltDiscordComponentMessage( + ...args, + )) as DiscordRuntimeFacadeModule["registerBuiltDiscordComponentMessage"]; + export async function autoBindSpawnedDiscordSubagent(params: { cfg?: OpenClawConfig; accountId?: string; diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts index 9bf55f446ed..e4c7d5cb8d9 100644 --- a/src/plugin-sdk/entrypoints.ts +++ b/src/plugin-sdk/entrypoints.ts @@ -16,6 +16,7 @@ export const supportedBundledFacadeSdkEntrypoints = [ "lmstudio-runtime", "memory-core-engine-runtime", "qa-runner-runtime", + "telegram-account", "tts-runtime", ] as const; diff --git a/src/plugin-sdk/telegram-account.test.ts b/src/plugin-sdk/telegram-account.test.ts new file mode 100644 index 00000000000..7f7088207ae --- /dev/null +++ b/src/plugin-sdk/telegram-account.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const apiModule = { + resolveTelegramAccount: vi.fn(() => ({ + accountId: "default", + config: {}, + enabled: true, + token: "token", + tokenSource: "config", + })), + }; + + return { + apiModule, + loadBundledPluginPublicSurfaceModuleSync: vi.fn(() => apiModule), + }; +}); + +vi.mock("./facade-loader.js", () => ({ + loadBundledPluginPublicSurfaceModuleSync: mocks.loadBundledPluginPublicSurfaceModuleSync, +})); + +describe("telegram account plugin-sdk compatibility facade", () => { + it("forwards account resolution through Telegram's public surface", async () => { + const { resolveTelegramAccount } = await import("./telegram-account.js"); + const cfg = { channels: { telegram: { botToken: "token" } } }; + + const account = resolveTelegramAccount({ cfg, accountId: "default" }); + + expect(mocks.loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ + dirName: "telegram", + artifactBasename: "api.js", + }); + expect(mocks.apiModule.resolveTelegramAccount).toHaveBeenCalledWith({ + cfg, + accountId: "default", + }); + expect(account).toEqual( + expect.objectContaining({ + accountId: "default", + token: "token", + }), + ); + }); +}); diff --git a/src/plugin-sdk/telegram-account.ts b/src/plugin-sdk/telegram-account.ts new file mode 100644 index 00000000000..915fbf754c9 --- /dev/null +++ b/src/plugin-sdk/telegram-account.ts @@ -0,0 +1,26 @@ +import type { OpenClawConfig } from "./config-types.js"; +import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-loader.js"; + +export type { ResolvedTelegramAccount } from "../../extensions/telegram/api.js"; + +type TelegramAccountFacadeModule = { + resolveTelegramAccount: typeof import("../../extensions/telegram/api.js").resolveTelegramAccount; +}; + +function loadTelegramAccountFacadeModule(): TelegramAccountFacadeModule { + return loadBundledPluginPublicSurfaceModuleSync({ + dirName: "telegram", + artifactBasename: "api.js", + }); +} + +/** + * @deprecated Compatibility facade for plugin code that needs Telegram account resolution. + * New channel plugins should prefer injected runtime helpers and generic SDK subpaths. + */ +export function resolveTelegramAccount(params: { + cfg: OpenClawConfig; + accountId?: string | null; +}): ReturnType { + return loadTelegramAccountFacadeModule().resolveTelegramAccount(params); +} diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index e4233d04a9a..a5eddfaaf6e 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -101,6 +101,25 @@ export const PLUGIN_COMPAT_RECORDS = [ "src/plugins/captured-registration.test.ts", ], }, + { + code: "bundled-channel-sdk-compat-facades", + status: "active", + owner: "sdk", + introduced: "2026-04-28", + replacement: + "generic channel SDK subpaths or plugin-local `api.ts` / `runtime-api.ts` barrels for new plugins", + docsPath: "/plugins/sdk-overview", + surfaces: [ + "openclaw/plugin-sdk/discord component message helpers", + "openclaw/plugin-sdk/telegram-account resolveTelegramAccount", + ], + diagnostics: ["plugin SDK compatibility registry"], + tests: [ + "src/plugin-sdk/discord.test.ts", + "src/plugin-sdk/telegram-account.test.ts", + "src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts", + ], + }, { code: "bundled-channel-config-schema-legacy", status: "deprecated",