fix(plugin-sdk): restore discord compatibility facade

This commit is contained in:
Peter Steinberger
2026-04-28 20:59:21 +01:00
parent d1a7612bd6
commit 3cad579c4e
13 changed files with 407 additions and 7 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugin SDK/Discord: restore a deprecated `openclaw/plugin-sdk/discord` compatibility facade and the legacy compat group-policy warning export for the published `@openclaw/discord@2026.3.13` package, covering its config, account, directory, status, and thread-binding imports while keeping new plugins on generic SDK subpaths. Fixes #73685; supersedes #73703. Thanks @rderickson9 and @SymbolStar.
- Channels/Discord: suppress duplicate gateway monitors when multiple enabled accounts resolve to the same bot token, preferring config tokens over default env fallback and reporting skipped duplicates as disabled. Supersedes #73608. Thanks @kagura-agent.
- Control UI/Talk: decode Google Live binary WebSocket JSON frames and stop queued browser audio on interruption or shutdown, so browser Talk leaves `Connecting Talk...` and barge-in no longer plays stale audio. Fixes #73601 and #73460; supersedes #73466. Thanks @Spolen23 and @WadydX.
- Channels/Discord: ignore stale route-shaped conversation bindings after a Discord channel is reconfigured to another agent, while preserving explicit focus and subagent bindings. Fixes #73626. Thanks @ramitrkar-hash.

View File

@@ -1,2 +1,2 @@
7da4f3439785b85e93740640efce5ca2e6eb0024139a6585641fb995f9b3830c plugin-sdk-api-baseline.json
91d40edb771792303d91a5d188ec1e6d77647c46866733ad65bf16d6a57348ba plugin-sdk-api-baseline.jsonl
46476e7b4fee105ca27aed9c769c507f70f02b8ce8586c135feb18e751db0de1 plugin-sdk-api-baseline.json
4bc1c0dc66d910c80694fa1a6b7ba3ab488bf737b3566e53b8a5857c16d2e0b1 plugin-sdk-api-baseline.jsonl

View File

@@ -545,10 +545,12 @@ surface. The full list of 200+ entrypoints lives in
`scripts/lib/plugin-sdk-entrypoints.json`.
Reserved bundled-plugin helper seams have been retired from the public SDK
export map. Owner-specific helpers live inside the owning plugin package; shared
host behavior should move through generic SDK contracts such as
`plugin-sdk/gateway-runtime`, `plugin-sdk/security-runtime`, and
`plugin-sdk/plugin-config-runtime`.
export map except for explicitly documented compatibility facades such as the
deprecated `plugin-sdk/discord` shim retained for the published
`@openclaw/discord@2026.3.13` package. Owner-specific helpers live inside the
owning plugin package; shared host behavior should move through generic SDK
contracts such as `plugin-sdk/gateway-runtime`, `plugin-sdk/security-runtime`,
and `plugin-sdk/plugin-config-runtime`.
Use the narrowest import that matches the job. If you cannot find an export,
check the source at `src/plugin-sdk/` or ask maintainers which generic contract

View File

@@ -50,6 +50,10 @@ A small set of bundled-plugin helper seams still appear in the generated export
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.
</Warning>
## Subpath reference

View File

@@ -84,6 +84,7 @@ 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/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

@@ -664,6 +664,10 @@
"types": "./dist/plugin-sdk/direct-dm-guard-policy.d.ts",
"default": "./dist/plugin-sdk/direct-dm-guard-policy.js"
},
"./plugin-sdk/discord": {
"types": "./dist/plugin-sdk/discord.d.ts",
"default": "./dist/plugin-sdk/discord.js"
},
"./plugin-sdk/device-bootstrap": {
"types": "./dist/plugin-sdk/device-bootstrap.d.ts",
"default": "./dist/plugin-sdk/device-bootstrap.js"

View File

@@ -149,6 +149,7 @@
"direct-dm",
"direct-dm-access",
"direct-dm-guard-policy",
"discord",
"device-bootstrap",
"diagnostic-runtime",
"error-runtime",

View File

@@ -25,6 +25,7 @@ export {
buildOpenGroupPolicyWarning,
collectAllowlistProviderGroupPolicyWarnings,
collectAllowlistProviderRestrictSendersWarnings,
collectOpenGroupPolicyConfiguredRouteWarnings,
collectOpenGroupPolicyRestrictSendersWarnings,
collectOpenGroupPolicyRouteAllowlistWarnings,
collectOpenProviderGroupPolicyWarnings,

View File

@@ -61,6 +61,7 @@ export {
export { formatAllowFromLowercase, formatNormalizedAllowFromEntries } from "./allow-from.js";
export * from "./channel-config-schema.js";
export * from "./channel-policy.js";
export { collectOpenGroupPolicyConfiguredRouteWarnings } from "./channel-policy.js";
export * from "./reply-history.js";
export * from "./directory-runtime.js";
export { mapAllowlistResolutionInputs } from "./allow-from.js";

View File

@@ -0,0 +1,135 @@
import { describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const runtimeConfig = { channels: { discord: { token: "token" } } };
const apiModule = {
collectDiscordStatusIssues: vi.fn(() => []),
discordOnboardingAdapter: { kind: "legacy-onboarding" },
inspectDiscordAccount: vi.fn(() => ({ accountId: "default" })),
listDiscordAccountIds: vi.fn(() => ["default"]),
listDiscordDirectoryGroupsFromConfig: vi.fn(() => []),
listDiscordDirectoryPeersFromConfig: vi.fn(() => []),
looksLikeDiscordTargetId: vi.fn(() => true),
normalizeDiscordMessagingTarget: vi.fn(() => "channel:123"),
normalizeDiscordOutboundTarget: vi.fn(() => ({ ok: true, to: "channel:123" })),
resolveDefaultDiscordAccountId: vi.fn(() => "default"),
resolveDiscordAccount: vi.fn(() => ({
accountId: "default",
config: {},
enabled: true,
token: "token",
tokenSource: "config",
})),
resolveDiscordGroupRequireMention: vi.fn(() => true),
resolveDiscordGroupToolPolicy: vi.fn(() => undefined),
};
const runtimeModule = {
autoBindSpawnedDiscordSubagent: vi.fn(async (params) => ({
accountId: params.accountId ?? "default",
channelId: "123",
targetKind: "subagent",
targetSessionKey: params.childSessionKey,
threadId: "456",
cfg: params.cfg,
})),
collectDiscordAuditChannelIds: vi.fn(() => ({ channelIds: [], unresolvedChannels: [] })),
listThreadBindingsBySessionKey: vi.fn(() => []),
unbindThreadBindingsBySessionKey: vi.fn(() => []),
};
return {
apiModule,
runtimeModule,
runtimeConfig,
loadBundledPluginPublicSurfaceModuleSync: vi.fn((params: { artifactBasename: string }) => {
if (params.artifactBasename === "runtime-api.js") {
return runtimeModule;
}
return apiModule;
}),
};
});
vi.mock("./facade-loader.js", () => ({
createLazyFacadeObjectValue: (load: () => object) =>
new Proxy(
{},
{
get(_target, property) {
return Reflect.get(load(), property);
},
},
),
loadBundledPluginPublicSurfaceModuleSync: mocks.loadBundledPluginPublicSurfaceModuleSync,
}));
vi.mock("./runtime-config-snapshot.js", () => ({
getRuntimeConfig: () => mocks.runtimeConfig,
getRuntimeConfigSnapshot: () => mocks.runtimeConfig,
}));
describe("discord plugin-sdk compatibility facade", () => {
it("exports the @openclaw/discord 2026.3.13 import surface", async () => {
const discordSdk = await import("./discord.js");
for (const exportName of [
"DEFAULT_ACCOUNT_ID",
"DiscordConfigSchema",
"PAIRING_APPROVED_MESSAGE",
"applyAccountNameToChannelSection",
"autoBindSpawnedDiscordSubagent",
"buildChannelConfigSchema",
"buildComputedAccountStatusSnapshot",
"buildTokenChannelStatusSummary",
"collectDiscordAuditChannelIds",
"collectDiscordStatusIssues",
"discordOnboardingAdapter",
"emptyPluginConfigSchema",
"getChatChannelMeta",
"inspectDiscordAccount",
"listDiscordAccountIds",
"listDiscordDirectoryGroupsFromConfig",
"listDiscordDirectoryPeersFromConfig",
"listThreadBindingsBySessionKey",
"looksLikeDiscordTargetId",
"migrateBaseNameToDefaultAccount",
"normalizeAccountId",
"normalizeDiscordMessagingTarget",
"normalizeDiscordOutboundTarget",
"projectCredentialSnapshotFields",
"resolveConfiguredFromCredentialStatuses",
"resolveDefaultDiscordAccountId",
"resolveDiscordAccount",
"resolveDiscordGroupRequireMention",
"resolveDiscordGroupToolPolicy",
"unbindThreadBindingsBySessionKey",
]) {
expect(discordSdk).toHaveProperty(exportName);
}
});
it("keeps legacy Discord subagent auto-bind calls working without cfg", async () => {
const { autoBindSpawnedDiscordSubagent } = await import("./discord.js");
const binding = await autoBindSpawnedDiscordSubagent({
agentId: "agent",
channel: "discord",
childSessionKey: "child",
});
expect(mocks.runtimeModule.autoBindSpawnedDiscordSubagent).toHaveBeenCalledWith(
expect.objectContaining({
agentId: "agent",
cfg: mocks.runtimeConfig,
childSessionKey: "child",
}),
);
expect(binding).toEqual(
expect.objectContaining({
cfg: mocks.runtimeConfig,
targetKind: "subagent",
targetSessionKey: "child",
}),
);
});
});

248
src/plugin-sdk/discord.ts Normal file
View File

@@ -0,0 +1,248 @@
import type {
ChannelAccountSnapshot,
ChannelGroupContext,
ChannelMessageActionAdapter,
ChannelStatusIssue,
} from "./channel-contract.js";
import type { ChannelPlugin } from "./channel-core.js";
import type { OpenClawConfig } from "./config-types.js";
import {
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSync,
} from "./facade-loader.js";
import { getRuntimeConfig, getRuntimeConfigSnapshot } from "./runtime-config-snapshot.js";
export type { ChannelMessageActionAdapter, ChannelMessageActionName } from "./channel-contract.js";
export type { ChannelPlugin } from "./channel-core.js";
export type { OpenClawConfig } from "./config-types.js";
export type { OpenClawPluginApi, PluginRuntime } from "./channel-plugin-common.js";
export {
DEFAULT_ACCOUNT_ID,
applyAccountNameToChannelSection,
buildChannelConfigSchema,
emptyPluginConfigSchema,
getChatChannelMeta,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
} from "./channel-plugin-common.js";
export {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
} from "./channel-status.js";
export { DiscordConfigSchema } from "./bundled-channel-config-schema.js";
export type DiscordAccountConfig = NonNullable<NonNullable<OpenClawConfig["channels"]>["discord"]>;
export type ResolvedDiscordAccount = {
accountId: string;
enabled: boolean;
name?: string;
token: string;
tokenSource: "env" | "config" | "none";
config: DiscordAccountConfig;
};
export type DiscordOutboundTargetResolution =
| { ok: true; to: string }
| { ok: false; error: Error };
export type ThreadBindingTargetKind = "subagent" | "acp";
export type ThreadBindingRecord = {
accountId: string;
threadId: string;
channelId?: string;
targetKind: ThreadBindingTargetKind;
targetSessionKey: string;
[key: string]: unknown;
};
type DirectoryConfigParams = {
cfg: OpenClawConfig;
accountId?: string | null;
};
type DiscordApiFacadeModule = {
collectDiscordStatusIssues: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
discordOnboardingAdapter?: NonNullable<ChannelPlugin<ResolvedDiscordAccount>["setup"]>;
inspectDiscordAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => unknown;
listDiscordAccountIds: (cfg: OpenClawConfig) => string[];
listDiscordDirectoryGroupsFromConfig: (
params: DirectoryConfigParams,
) => unknown[] | Promise<unknown[]>;
listDiscordDirectoryPeersFromConfig: (
params: DirectoryConfigParams,
) => unknown[] | Promise<unknown[]>;
looksLikeDiscordTargetId: (raw: string) => boolean;
normalizeDiscordMessagingTarget: (raw: string) => string | undefined;
normalizeDiscordOutboundTarget: (to?: string) => DiscordOutboundTargetResolution;
resolveDefaultDiscordAccountId: (cfg: OpenClawConfig) => string;
resolveDiscordAccount: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => ResolvedDiscordAccount;
resolveDiscordGroupRequireMention: (params: ChannelGroupContext) => boolean | undefined;
resolveDiscordGroupToolPolicy: (params: ChannelGroupContext) => unknown;
};
type DiscordRuntimeFacadeModule = {
autoBindSpawnedDiscordSubagent: (params: {
cfg: OpenClawConfig;
accountId?: string;
channel?: string;
to?: string;
threadId?: string | number;
childSessionKey: string;
agentId: string;
label?: string;
boundBy?: string;
}) => Promise<ThreadBindingRecord | null>;
collectDiscordAuditChannelIds: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => unknown;
listThreadBindingsBySessionKey: (params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}) => ThreadBindingRecord[];
unbindThreadBindingsBySessionKey: (params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
reason?: string;
sendFarewell?: boolean;
farewellText?: string;
}) => ThreadBindingRecord[];
};
function loadDiscordApiFacadeModule(): DiscordApiFacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<DiscordApiFacadeModule>({
dirName: "discord",
artifactBasename: "api.js",
});
}
function loadDiscordRuntimeFacadeModule(): DiscordRuntimeFacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<DiscordRuntimeFacadeModule>({
dirName: "discord",
artifactBasename: "runtime-api.js",
});
}
function resolveCompatRuntimeConfig(params: { cfg?: OpenClawConfig }): OpenClawConfig {
return params.cfg ?? getRuntimeConfigSnapshot() ?? getRuntimeConfig();
}
export const discordOnboardingAdapter = createLazyFacadeObjectValue(
() => loadDiscordApiFacadeModule().discordOnboardingAdapter ?? {},
);
export function collectDiscordStatusIssues(
accounts: ChannelAccountSnapshot[],
): ChannelStatusIssue[] {
return loadDiscordApiFacadeModule().collectDiscordStatusIssues(accounts);
}
export function inspectDiscordAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): unknown {
return loadDiscordApiFacadeModule().inspectDiscordAccount(params);
}
export function listDiscordAccountIds(cfg: OpenClawConfig): string[] {
return loadDiscordApiFacadeModule().listDiscordAccountIds(cfg);
}
export function listDiscordDirectoryGroupsFromConfig(
params: DirectoryConfigParams,
): unknown[] | Promise<unknown[]> {
return loadDiscordApiFacadeModule().listDiscordDirectoryGroupsFromConfig(params);
}
export function listDiscordDirectoryPeersFromConfig(
params: DirectoryConfigParams,
): unknown[] | Promise<unknown[]> {
return loadDiscordApiFacadeModule().listDiscordDirectoryPeersFromConfig(params);
}
export function looksLikeDiscordTargetId(raw: string): boolean {
return loadDiscordApiFacadeModule().looksLikeDiscordTargetId(raw);
}
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
return loadDiscordApiFacadeModule().normalizeDiscordMessagingTarget(raw);
}
export function normalizeDiscordOutboundTarget(to?: string): DiscordOutboundTargetResolution {
return loadDiscordApiFacadeModule().normalizeDiscordOutboundTarget(to);
}
export function resolveDefaultDiscordAccountId(cfg: OpenClawConfig): string {
return loadDiscordApiFacadeModule().resolveDefaultDiscordAccountId(cfg);
}
export function resolveDiscordAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): ResolvedDiscordAccount {
return loadDiscordApiFacadeModule().resolveDiscordAccount(params);
}
export function resolveDiscordGroupRequireMention(
params: ChannelGroupContext,
): boolean | undefined {
return loadDiscordApiFacadeModule().resolveDiscordGroupRequireMention(params);
}
export function resolveDiscordGroupToolPolicy(params: ChannelGroupContext): unknown {
return loadDiscordApiFacadeModule().resolveDiscordGroupToolPolicy(params);
}
export function collectDiscordAuditChannelIds(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): unknown {
return loadDiscordRuntimeFacadeModule().collectDiscordAuditChannelIds(params);
}
export async function autoBindSpawnedDiscordSubagent(params: {
cfg?: OpenClawConfig;
accountId?: string;
channel?: string;
to?: string;
threadId?: string | number;
childSessionKey: string;
agentId: string;
label?: string;
boundBy?: string;
}): Promise<ThreadBindingRecord | null> {
return await loadDiscordRuntimeFacadeModule().autoBindSpawnedDiscordSubagent({
...params,
cfg: resolveCompatRuntimeConfig(params),
});
}
export function listThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}): ThreadBindingRecord[] {
return loadDiscordRuntimeFacadeModule().listThreadBindingsBySessionKey(params);
}
export function unbindThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
reason?: string;
sendFarewell?: boolean;
farewellText?: string;
}): ThreadBindingRecord[] {
return loadDiscordRuntimeFacadeModule().unbindThreadBindingsBySessionKey(params);
}

View File

@@ -11,6 +11,7 @@ export const reservedBundledPluginSdkEntrypoints = [] as const;
// Supported SDK facades backed by bundled plugins. These are intentionally public
// until they move to generic, plugin-neutral contracts.
export const supportedBundledFacadeSdkEntrypoints = [
"discord",
"lmstudio",
"lmstudio-runtime",
"memory-core-engine-runtime",

View File

@@ -462,7 +462,7 @@ describe("plugin-sdk subpath exports", () => {
});
it("keeps removed bundled-channel aliases out of the public sdk list", () => {
const removedChannelAliases = new Set(["discord", "signal", "slack", "telegram", "whatsapp"]);
const removedChannelAliases = new Set(["signal", "slack", "telegram", "whatsapp"]);
const banned = pluginSdkSubpaths.filter((subpath) => removedChannelAliases.has(subpath));
expect(banned).toEqual([]);
});
@@ -640,6 +640,7 @@ describe("plugin-sdk subpath exports", () => {
expectSourceMentions("compat", [
"createPluginRuntimeStore",
"createScopedChannelConfigAdapter",
"collectOpenGroupPolicyConfiguredRouteWarnings",
"resolveControlCommandGate",
"delegateCompactionToRuntime",
]);