fix(plugin-sdk): restore channel compatibility facades

This commit is contained in:
Vincent Koc
2026-04-28 21:29:37 -07:00
parent 02c4249632
commit 9e34fb9feb
11 changed files with 172 additions and 4 deletions

View File

@@ -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.

View File

@@ -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.
</Warning>
## Subpath reference

View File

@@ -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 |

View File

@@ -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"

View File

@@ -281,6 +281,7 @@
"string-normalization-runtime",
"state-paths",
"target-resolver-runtime",
"telegram-account",
"telegram-command-config",
"text-autolink-runtime",
"tool-payload",

View File

@@ -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");

View File

@@ -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<ChannelPlugin<ResolvedDiscordAccount>["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;

View File

@@ -16,6 +16,7 @@ export const supportedBundledFacadeSdkEntrypoints = [
"lmstudio-runtime",
"memory-core-engine-runtime",
"qa-runner-runtime",
"telegram-account",
"tts-runtime",
] as const;

View File

@@ -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",
}),
);
});
});

View File

@@ -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<TelegramAccountFacadeModule>({
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<TelegramAccountFacadeModule["resolveTelegramAccount"]> {
return loadTelegramAccountFacadeModule().resolveTelegramAccount(params);
}

View File

@@ -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",