@openclaw/discord 2026.3.2 → 2026.3.8-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/discord";
3
3
  import { discordPlugin } from "./src/channel.js";
4
4
  import { setDiscordRuntime } from "./src/runtime.js";
5
5
  import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/discord",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.8-beta.1",
4
4
  "description": "OpenClaw Discord channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/discord";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import { discordPlugin } from "./channel.js";
4
4
  import { setDiscordRuntime } from "./runtime.js";
package/src/channel.ts CHANGED
@@ -1,15 +1,23 @@
1
+ import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
2
+ import {
3
+ buildAccountScopedDmSecurityPolicy,
4
+ collectOpenProviderGroupPolicyWarnings,
5
+ collectOpenGroupPolicyConfiguredRouteWarnings,
6
+ createScopedAccountConfigAccessors,
7
+ formatAllowFromLowercase,
8
+ } from "openclaw/plugin-sdk/compat";
1
9
  import {
2
10
  applyAccountNameToChannelSection,
11
+ buildComputedAccountStatusSnapshot,
3
12
  buildChannelConfigSchema,
4
13
  buildTokenChannelStatusSummary,
5
14
  collectDiscordAuditChannelIds,
6
15
  collectDiscordStatusIssues,
7
16
  DEFAULT_ACCOUNT_ID,
8
- deleteAccountFromConfigSection,
9
17
  discordOnboardingAdapter,
10
18
  DiscordConfigSchema,
11
- formatPairingApproveHint,
12
19
  getChatChannelMeta,
20
+ inspectDiscordAccount,
13
21
  listDiscordAccountIds,
14
22
  listDiscordDirectoryGroupsFromConfig,
15
23
  listDiscordDirectoryPeersFromConfig,
@@ -19,17 +27,16 @@ import {
19
27
  normalizeDiscordMessagingTarget,
20
28
  normalizeDiscordOutboundTarget,
21
29
  PAIRING_APPROVED_MESSAGE,
30
+ projectCredentialSnapshotFields,
31
+ resolveConfiguredFromCredentialStatuses,
22
32
  resolveDiscordAccount,
23
33
  resolveDefaultDiscordAccountId,
24
34
  resolveDiscordGroupRequireMention,
25
35
  resolveDiscordGroupToolPolicy,
26
- resolveOpenProviderRuntimeGroupPolicy,
27
- resolveDefaultGroupPolicy,
28
- setAccountEnabledInConfigSection,
29
36
  type ChannelMessageActionAdapter,
30
37
  type ChannelPlugin,
31
38
  type ResolvedDiscordAccount,
32
- } from "openclaw/plugin-sdk";
39
+ } from "openclaw/plugin-sdk/discord";
33
40
  import { getDiscordRuntime } from "./runtime.js";
34
41
 
35
42
  const meta = getChatChannelMeta("discord");
@@ -48,6 +55,22 @@ const discordMessageActions: ChannelMessageActionAdapter = {
48
55
  },
49
56
  };
50
57
 
58
+ const discordConfigAccessors = createScopedAccountConfigAccessors({
59
+ resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
60
+ resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
61
+ formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
62
+ resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
63
+ });
64
+
65
+ const discordConfigBase = createScopedChannelConfigBase({
66
+ sectionKey: "discord",
67
+ listAccountIds: listDiscordAccountIds,
68
+ resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
69
+ inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
70
+ defaultAccountId: resolveDefaultDiscordAccountId,
71
+ clearBaseFields: ["token", "name"],
72
+ });
73
+
51
74
  export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
52
75
  id: "discord",
53
76
  meta: {
@@ -78,24 +101,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
78
101
  reload: { configPrefixes: ["channels.discord"] },
79
102
  configSchema: buildChannelConfigSchema(DiscordConfigSchema),
80
103
  config: {
81
- listAccountIds: (cfg) => listDiscordAccountIds(cfg),
82
- resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
83
- defaultAccountId: (cfg) => resolveDefaultDiscordAccountId(cfg),
84
- setAccountEnabled: ({ cfg, accountId, enabled }) =>
85
- setAccountEnabledInConfigSection({
86
- cfg,
87
- sectionKey: "discord",
88
- accountId,
89
- enabled,
90
- allowTopLevel: true,
91
- }),
92
- deleteAccount: ({ cfg, accountId }) =>
93
- deleteAccountFromConfigSection({
94
- cfg,
95
- sectionKey: "discord",
96
- accountId,
97
- clearBaseFields: ["token", "name"],
98
- }),
104
+ ...discordConfigBase,
99
105
  isConfigured: (account) => Boolean(account.token?.trim()),
100
106
  describeAccount: (account) => ({
101
107
  accountId: account.accountId,
@@ -104,58 +110,49 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
104
110
  configured: Boolean(account.token?.trim()),
105
111
  tokenSource: account.tokenSource,
106
112
  }),
107
- resolveAllowFrom: ({ cfg, accountId }) =>
108
- (resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) =>
109
- String(entry),
110
- ),
111
- formatAllowFrom: ({ allowFrom }) =>
112
- allowFrom
113
- .map((entry) => String(entry).trim())
114
- .filter(Boolean)
115
- .map((entry) => entry.toLowerCase()),
116
- resolveDefaultTo: ({ cfg, accountId }) =>
117
- resolveDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
113
+ ...discordConfigAccessors,
118
114
  },
119
115
  security: {
120
116
  resolveDmPolicy: ({ cfg, accountId, account }) => {
121
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
122
- const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]);
123
- const allowFromPath = useAccountPath
124
- ? `channels.discord.accounts.${resolvedAccountId}.dm.`
125
- : "channels.discord.dm.";
126
- return {
127
- policy: account.config.dm?.policy ?? "pairing",
117
+ return buildAccountScopedDmSecurityPolicy({
118
+ cfg,
119
+ channelKey: "discord",
120
+ accountId,
121
+ fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
122
+ policy: account.config.dm?.policy,
128
123
  allowFrom: account.config.dm?.allowFrom ?? [],
129
- allowFromPath,
130
- approveHint: formatPairingApproveHint("discord"),
124
+ allowFromPathSuffix: "dm.",
131
125
  normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
132
- };
126
+ });
133
127
  },
134
128
  collectWarnings: ({ account, cfg }) => {
135
- const warnings: string[] = [];
136
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
137
- const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
138
- providerConfigPresent: cfg.channels?.discord !== undefined,
139
- groupPolicy: account.config.groupPolicy,
140
- defaultGroupPolicy,
141
- });
142
129
  const guildEntries = account.config.guilds ?? {};
143
130
  const guildsConfigured = Object.keys(guildEntries).length > 0;
144
131
  const channelAllowlistConfigured = guildsConfigured;
145
132
 
146
- if (groupPolicy === "open") {
147
- if (channelAllowlistConfigured) {
148
- warnings.push(
149
- `- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
150
- );
151
- } else {
152
- warnings.push(
153
- `- Discord guilds: groupPolicy="open" with no guild/channel allowlist; any channel can trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
154
- );
155
- }
156
- }
157
-
158
- return warnings;
133
+ return collectOpenProviderGroupPolicyWarnings({
134
+ cfg,
135
+ providerConfigPresent: cfg.channels?.discord !== undefined,
136
+ configuredGroupPolicy: account.config.groupPolicy,
137
+ collect: (groupPolicy) =>
138
+ collectOpenGroupPolicyConfiguredRouteWarnings({
139
+ groupPolicy,
140
+ routeAllowlistConfigured: channelAllowlistConfigured,
141
+ configureRouteAllowlist: {
142
+ surface: "Discord guilds",
143
+ openScope: "any channel not explicitly denied",
144
+ groupPolicyPath: "channels.discord.groupPolicy",
145
+ routeAllowlistPath: "channels.discord.guilds.<id>.channels",
146
+ },
147
+ missingRouteAllowlist: {
148
+ surface: "Discord guilds",
149
+ openBehavior:
150
+ "with no guild/channel allowlist; any channel can trigger (mention-gated)",
151
+ remediation:
152
+ 'Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels',
153
+ },
154
+ }),
155
+ });
159
156
  },
160
157
  },
161
158
  groups: {
@@ -302,10 +299,11 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
302
299
  textChunkLimit: 2000,
303
300
  pollMaxOptions: 10,
304
301
  resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
305
- sendText: async ({ to, text, accountId, deps, replyToId, silent }) => {
302
+ sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
306
303
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
307
304
  const result = await send(to, text, {
308
305
  verbose: false,
306
+ cfg,
309
307
  replyTo: replyToId ?? undefined,
310
308
  accountId: accountId ?? undefined,
311
309
  silent: silent ?? undefined,
@@ -313,6 +311,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
313
311
  return { channel: "discord", ...result };
314
312
  },
315
313
  sendMedia: async ({
314
+ cfg,
316
315
  to,
317
316
  text,
318
317
  mediaUrl,
@@ -325,6 +324,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
325
324
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
326
325
  const result = await send(to, text, {
327
326
  verbose: false,
327
+ cfg,
328
328
  mediaUrl,
329
329
  mediaLocalRoots,
330
330
  replyTo: replyToId ?? undefined,
@@ -333,8 +333,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
333
333
  });
334
334
  return { channel: "discord", ...result };
335
335
  },
336
- sendPoll: async ({ to, poll, accountId, silent }) =>
336
+ sendPoll: async ({ cfg, to, poll, accountId, silent }) =>
337
337
  await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, {
338
+ cfg,
338
339
  accountId: accountId ?? undefined,
339
340
  silent: silent ?? undefined,
340
341
  }),
@@ -386,19 +387,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
386
387
  return { ...audit, unresolvedChannels };
387
388
  },
388
389
  buildAccountSnapshot: ({ account, runtime, probe, audit }) => {
389
- const configured = Boolean(account.token?.trim());
390
+ const configured =
391
+ resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim());
390
392
  const app = runtime?.application ?? (probe as { application?: unknown })?.application;
391
393
  const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot;
392
- return {
394
+ const base = buildComputedAccountStatusSnapshot({
393
395
  accountId: account.accountId,
394
396
  name: account.name,
395
397
  enabled: account.enabled,
396
398
  configured,
397
- tokenSource: account.tokenSource,
398
- running: runtime?.running ?? false,
399
- lastStartAt: runtime?.lastStartAt ?? null,
400
- lastStopAt: runtime?.lastStopAt ?? null,
401
- lastError: runtime?.lastError ?? null,
399
+ runtime,
400
+ probe,
401
+ });
402
+ return {
403
+ ...base,
404
+ ...projectCredentialSnapshotFields(account),
402
405
  connected: runtime?.connected ?? false,
403
406
  reconnectAttempts: runtime?.reconnectAttempts,
404
407
  lastConnectedAt: runtime?.lastConnectedAt ?? null,
@@ -406,10 +409,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
406
409
  lastEventAt: runtime?.lastEventAt ?? null,
407
410
  application: app ?? undefined,
408
411
  bot: bot ?? undefined,
409
- probe,
410
412
  audit,
411
- lastInboundAt: runtime?.lastInboundAt ?? null,
412
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
413
413
  };
414
414
  },
415
415
  },
package/src/runtime.ts CHANGED
@@ -1,14 +1,6 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2
+ import type { PluginRuntime } from "openclaw/plugin-sdk/discord";
2
3
 
3
- let runtime: PluginRuntime | null = null;
4
-
5
- export function setDiscordRuntime(next: PluginRuntime) {
6
- runtime = next;
7
- }
8
-
9
- export function getDiscordRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("Discord runtime not initialized");
12
- }
13
- return runtime;
14
- }
4
+ const { setRuntime: setDiscordRuntime, getRuntime: getDiscordRuntime } =
5
+ createPluginRuntimeStore<PluginRuntime>("Discord runtime not initialized");
6
+ export { getDiscordRuntime, setDiscordRuntime };
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { registerDiscordSubagentHooks } from "./subagent-hooks.js";
4
4
 
@@ -35,7 +35,7 @@ const hookMocks = vi.hoisted(() => ({
35
35
  unbindThreadBindingsBySessionKey: vi.fn(() => []),
36
36
  }));
37
37
 
38
- vi.mock("openclaw/plugin-sdk", () => ({
38
+ vi.mock("openclaw/plugin-sdk/discord", () => ({
39
39
  resolveDiscordAccount: hookMocks.resolveDiscordAccount,
40
40
  autoBindSpawnedDiscordSubagent: hookMocks.autoBindSpawnedDiscordSubagent,
41
41
  listThreadBindingsBySessionKey: hookMocks.listThreadBindingsBySessionKey,
@@ -1,10 +1,10 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
2
  import {
3
3
  autoBindSpawnedDiscordSubagent,
4
4
  listThreadBindingsBySessionKey,
5
5
  resolveDiscordAccount,
6
6
  unbindThreadBindingsBySessionKey,
7
- } from "openclaw/plugin-sdk";
7
+ } from "openclaw/plugin-sdk/discord";
8
8
 
9
9
  function summarizeError(err: unknown): string {
10
10
  if (err instanceof Error) {