@zhin.js/adapter-discord 1.1.18 → 1.1.19

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +10 -3
  3. package/adapters/discord/index.js +3 -23
  4. package/adapters/discord/index.ts +2 -27
  5. package/agents/discord/skills/discord/tools/send_embed/index.js +1 -1
  6. package/agents/discord/skills/discord/tools/send_embed/index.ts +1 -1
  7. package/agents/discord/skills/discord-guild/tools/create_thread/index.js +6 -2
  8. package/agents/discord/skills/discord-guild/tools/create_thread/index.ts +5 -2
  9. package/agents/discord/skills/discord-guild/tools/forum_post/index.js +1 -1
  10. package/agents/discord/skills/discord-guild/tools/forum_post/index.ts +1 -1
  11. package/commands/discord/endpoint/definition.js +0 -2
  12. package/commands/discord/endpoint/definition.ts +0 -2
  13. package/lib/adapter.d.ts +14 -0
  14. package/lib/adapter.js +55 -0
  15. package/lib/client.d.ts +9 -9
  16. package/lib/client.js +0 -2
  17. package/lib/console-spec.js +9 -0
  18. package/lib/console.d.ts +1 -7
  19. package/lib/console.js +2 -5
  20. package/lib/endpoint.d.ts +36 -32
  21. package/lib/endpoint.js +174 -170
  22. package/lib/gateway.d.ts +6 -124
  23. package/lib/gateway.js +88 -63
  24. package/lib/index.d.ts +4 -3
  25. package/lib/index.js +3 -2
  26. package/lib/protocol.d.ts +4 -0
  27. package/lib/protocol.js +1 -0
  28. package/lib/webhook.d.ts +2 -3
  29. package/lib/webhook.js +42 -30
  30. package/package.json +18 -18
  31. package/plugin.js +1 -4
  32. package/src/adapter.ts +38 -0
  33. package/src/client.ts +6 -12
  34. package/src/console-spec.ts +9 -0
  35. package/src/console.ts +2 -6
  36. package/src/endpoint.ts +197 -172
  37. package/src/gateway.ts +103 -151
  38. package/src/index.ts +2 -3
  39. package/src/protocol.ts +5 -0
  40. package/src/webhook.ts +42 -32
  41. package/lib/discord-runtime-state.d.ts +0 -1
  42. package/lib/discord-runtime-state.js +0 -6
  43. package/src/discord-runtime-state.ts +0 -7
package/lib/gateway.d.ts CHANGED
@@ -1,133 +1,15 @@
1
- import { GatewayIntentBits, type RESTOptions, type MessageCreateOptions } from 'discord.js';
1
+ import type { EndpointEventSink } from 'zhin.js/adapter';
2
+ import { Client, GatewayIntentBits, type RESTOptions, type ClientOptions, type MessageCreateOptions } from 'discord.js';
2
3
  import { type DiscordButtonInbound, type DiscordInboundMessage, type DiscordOutboundBody, type ResolvedDiscordGatewayConfig } from './protocol.js';
3
4
  export declare const DEFAULT_INTENTS: GatewayIntentBits[];
4
- /** Minimal client surface used by the endpoint (real discord.js or test mock). */
5
- export interface DiscordClientTransport {
6
- isReady?(): boolean;
7
- login(token: string): Promise<string>;
8
- destroy(): Promise<void>;
9
- on(event: string, listener: (...args: unknown[]) => void): void;
10
- once(event: string, listener: (...args: unknown[]) => void): void;
11
- removeAllListeners(): void;
12
- readonly user?: {
13
- readonly id: string;
14
- readonly tag?: string;
15
- setActivity?(name: string, options?: {
16
- type?: number;
17
- url?: string;
18
- }): void;
19
- } | null;
20
- channels: {
21
- fetch(id: string): Promise<{
22
- id: string;
23
- type: number;
24
- isTextBased(): boolean;
25
- send?(options: MessageCreateOptions): Promise<{
26
- id: string;
27
- }>;
28
- messages?: {
29
- fetch(id: string): Promise<{
30
- delete(): Promise<unknown>;
31
- react(emoji: string): Promise<unknown>;
32
- reactions: {
33
- resolve(emoji: unknown): {
34
- users: {
35
- remove(userId: string): Promise<unknown>;
36
- };
37
- } | null;
38
- cache: {
39
- find(fn: (r: {
40
- emoji: {
41
- toString(): string;
42
- name?: string | null;
43
- id?: string | null;
44
- };
45
- }) => boolean): {
46
- users: {
47
- remove(userId: string): Promise<unknown>;
48
- };
49
- } | undefined;
50
- };
51
- };
52
- }>;
53
- };
54
- threads?: {
55
- create(options: Record<string, unknown>): Promise<{
56
- id: string;
57
- }>;
58
- };
59
- availableTags?: Array<{
60
- id: string;
61
- name: string;
62
- }>;
63
- } | null>;
64
- };
65
- guilds: {
66
- fetch(id: string): Promise<{
67
- id: string;
68
- name: string;
69
- ownerId: string;
70
- memberCount: number;
71
- createdAt?: Date | null;
72
- iconURL?(options?: {
73
- size?: number;
74
- }): string | null;
75
- roles: {
76
- fetch(): Promise<unknown>;
77
- cache: Map<string, {
78
- id: string;
79
- name: string;
80
- hexColor: string;
81
- position: number;
82
- permissions: {
83
- bitfield: bigint;
84
- };
85
- }> | {
86
- map(fn: (role: {
87
- id: string;
88
- name: string;
89
- hexColor: string;
90
- position: number;
91
- permissions: {
92
- bitfield: bigint;
93
- };
94
- }) => unknown): unknown[];
95
- };
96
- };
97
- members: {
98
- fetch(userId: string | {
99
- limit?: number;
100
- }): Promise<unknown>;
101
- ban(userId: string, options?: {
102
- reason?: string;
103
- deleteMessageSeconds?: number;
104
- }): Promise<unknown>;
105
- unban(userId: string, reason?: string): Promise<unknown>;
106
- };
107
- }>;
108
- cache: {
109
- values(): IterableIterator<{
110
- id: string;
111
- name: string;
112
- channels: {
113
- cache: {
114
- values(): IterableIterator<{
115
- id: string;
116
- name?: string;
117
- type: number;
118
- }>;
119
- };
120
- };
121
- }>;
122
- };
123
- };
124
- }
125
- export type CreateDiscordClient = (intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>) => DiscordClientTransport;
126
- export declare function defaultCreateClient(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): DiscordClientTransport;
5
+ /** Native SDK surface; tests inject transport below the Client rather than replacing its identity. */
6
+ export type DiscordClientTransport = Client;
7
+ export declare function discordClientOptions(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): ClientOptions;
127
8
  export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
128
9
  export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
129
10
  export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;
130
11
  export interface DiscordGatewayConnectHandlers {
12
+ readonly receive: EndpointEventSink['run'];
131
13
  onPlatformEvent(name: string, event: unknown): void;
132
14
  onMessage(msg: DiscordInboundMessage): void;
133
15
  onButton(interaction: DiscordButtonInbound): void;
package/lib/gateway.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DiscordFaultProxyStrategy } from './gateway-fault-proxy.js';
2
- import { createReadStream, promises as fs } from 'node:fs';
2
+ import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { Client, Partials, GatewayIntentBits, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, REST, Routes, PermissionFlagsBits, } from 'discord.js';
4
+ import { Partials, GatewayIntentBits, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, REST, Routes, PermissionFlagsBits, } from 'discord.js';
5
5
  import { formatCompact, getLogger } from '@zhin.js/logger';
6
6
  import { activityTypeCode, resolveChannelKind, } from './protocol.js';
7
7
  const logger = getLogger('discord');
@@ -13,8 +13,8 @@ export const DEFAULT_INTENTS = [
13
13
  GatewayIntentBits.GuildMembers,
14
14
  GatewayIntentBits.GuildMessageReactions,
15
15
  ];
16
- export function defaultCreateClient(intents, gatewayFaultProxyUrl, restOptions = {}) {
17
- return new Client({ rest: { ...restOptions, retries: 0, rejectOnRateLimit: () => true }, intents: [...intents], partials: [Partials.Channel], ...(gatewayFaultProxyUrl ? { ws: { buildStrategy: manager => new DiscordFaultProxyStrategy(manager, gatewayFaultProxyUrl) } } : {}) });
16
+ export function discordClientOptions(intents, gatewayFaultProxyUrl, restOptions = {}) {
17
+ return { rest: { ...restOptions, retries: 0, rejectOnRateLimit: () => true }, intents: [...intents], partials: [Partials.Channel], ...(gatewayFaultProxyUrl ? { ws: { buildStrategy: manager => new DiscordFaultProxyStrategy(manager, gatewayFaultProxyUrl) } } : {}) };
18
18
  }
19
19
  export function resolveSenderRole(msg) {
20
20
  if (msg.isGuildOwner)
@@ -55,6 +55,8 @@ export function normalizeDiscordMessage(raw) {
55
55
  }
56
56
  return {
57
57
  id: msg.id,
58
+ event: raw,
59
+ platformSentAt: msg.createdTimestamp,
58
60
  content: msg.content ?? '',
59
61
  channelId: msg.channel.id,
60
62
  channelKind: resolveChannelKind(msg.channel.type),
@@ -120,7 +122,7 @@ export async function toMessageCreateOptions(body) {
120
122
  }));
121
123
  }
122
124
  else if (file.file && await fileExists(file.file)) {
123
- files.push(new AttachmentBuilder(createReadStream(file.file), {
125
+ files.push(new AttachmentBuilder(file.file, {
124
126
  name: file.name || path.basename(file.file),
125
127
  }));
126
128
  }
@@ -211,70 +213,81 @@ export async function connectDiscordGatewayClient(client, config, handlers, sign
211
213
  signal?.addEventListener('abort', onAbort, { once: true });
212
214
  const releaseAbort = () => signal?.removeEventListener('abort', onAbort);
213
215
  client.on('messageCreate', (raw) => {
214
- handlers.onPlatformEvent('messageCreate', raw);
215
- const msg = normalizeDiscordMessage(raw);
216
- if (!msg)
217
- return;
218
- // clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
219
- const botId = client.user?.id;
220
- const mentions = raw.mentions;
221
- const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
222
- handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
216
+ void handlers.receive(() => {
217
+ handlers.onPlatformEvent('messageCreate', raw);
218
+ const msg = normalizeDiscordMessage(raw);
219
+ if (!msg)
220
+ return;
221
+ // clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
222
+ const botId = client.user?.id;
223
+ const mentions = raw.mentions;
224
+ const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
225
+ handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
226
+ }).catch(() => logger.debug('Native callback was not admitted'));
223
227
  });
224
228
  client.on('interactionCreate', (raw) => {
225
- handlers.onPlatformEvent('interactionCreate', raw);
226
- const interaction = raw;
227
- const isButton = interaction.isButton?.() === true;
228
- logger.info(formatCompact({ op: 'discord_gateway_interaction_received', button: isButton, cachedChannel: !!interaction.channel, hasChannelId: !!interaction.channelId }));
229
- if (!isButton)
230
- return;
231
- void interaction.deferUpdate?.().then(() => {
232
- logger.info(formatCompact({ op: 'discord_gateway_button_ack', status: 'confirmed' }));
233
- }).catch(() => {
234
- // SDK errors may contain token-bearing callback URLs; log no raw error.
235
- logger.warn(formatCompact({ op: 'discord_gateway_button_ack', status: 'failed' }));
236
- });
237
- const channelId = interaction.channelId ?? interaction.channel?.id;
238
- if (!channelId) {
239
- logger.warn(formatCompact({ op: 'discord_gateway_button_dropped', reason: 'missing_channel_id' }));
240
- return;
241
- }
242
- handlers.onButton({
243
- id: interaction.id,
244
- customId: interaction.customId,
245
- channelId,
246
- channelKind: interaction.channel ? resolveChannelKind(interaction.channel.type) : interaction.guildId ? 'channel' : 'private',
247
- ...(interaction.guildId ? { guildId: interaction.guildId } : {}),
248
- userId: interaction.user.id,
249
- userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
250
- sourceMessageId: interaction.message?.id,
251
- });
229
+ void handlers.receive(() => {
230
+ handlers.onPlatformEvent('interactionCreate', raw);
231
+ const interaction = raw;
232
+ const isButton = interaction.isButton?.() === true;
233
+ logger.info(formatCompact({ op: 'discord_gateway_interaction_received', button: isButton, cachedChannel: !!interaction.channel, hasChannelId: !!interaction.channelId }));
234
+ if (!isButton)
235
+ return;
236
+ const ack = interaction.deferUpdate?.().then(() => {
237
+ logger.info(formatCompact({ op: 'discord_gateway_button_ack', status: 'confirmed' }));
238
+ }).catch(() => {
239
+ // SDK errors may contain token-bearing callback URLs; log no raw error.
240
+ logger.warn(formatCompact({ op: 'discord_gateway_button_ack', status: 'failed' }));
241
+ });
242
+ const channelId = interaction.channelId ?? interaction.channel?.id;
243
+ if (!channelId) {
244
+ logger.warn(formatCompact({ op: 'discord_gateway_button_dropped', reason: 'missing_channel_id' }));
245
+ return ack ? awaitOwnedAck(ack, signal) : undefined;
246
+ }
247
+ handlers.onButton({
248
+ event: raw,
249
+ platformSentAt: raw.createdTimestamp,
250
+ id: interaction.id,
251
+ customId: interaction.customId,
252
+ channelId,
253
+ channelKind: interaction.channel ? resolveChannelKind(interaction.channel.type) : interaction.guildId ? 'channel' : 'private',
254
+ ...(interaction.guildId ? { guildId: interaction.guildId } : {}),
255
+ userId: interaction.user.id,
256
+ userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
257
+ sourceMessageId: interaction.message?.id,
258
+ });
259
+ return ack ? awaitOwnedAck(ack, signal) : undefined;
260
+ }).catch(() => logger.debug('Native callback was not admitted'));
252
261
  });
253
262
  client.on('guildMemberAdd', (raw) => {
254
- handlers.onPlatformEvent('guildMemberAdd', raw);
255
- const member = raw;
256
- const guildId = member.guild?.id;
257
- const userId = member.user?.id;
258
- if (!guildId || !userId)
259
- return;
260
- handlers.onGuildMemberAdd?.({
261
- guildId,
262
- userId,
263
- userName: member.user?.username || member.user?.displayName,
264
- });
263
+ void handlers.receive(() => {
264
+ handlers.onPlatformEvent('guildMemberAdd', raw);
265
+ const member = raw;
266
+ const guildId = member.guild?.id;
267
+ const userId = member.user?.id;
268
+ if (!guildId || !userId)
269
+ return;
270
+ handlers.onGuildMemberAdd?.({
271
+ guildId,
272
+ userId,
273
+ userName: member.user?.username || member.user?.displayName,
274
+ });
275
+ }).catch(() => logger.debug('Native callback was not admitted'));
265
276
  });
266
277
  client.on('guildMemberRemove', (raw) => {
267
- handlers.onPlatformEvent('guildMemberRemove', raw);
268
- const member = raw;
269
- const guildId = member.guild?.id;
270
- const userId = member.user?.id;
271
- if (!guildId || !userId)
272
- return;
273
- handlers.onGuildMemberRemove?.({
274
- guildId,
275
- userId,
276
- userName: member.user?.username || member.user?.displayName,
277
- });
278
+ void handlers.receive(() => {
279
+ handlers.onPlatformEvent('guildMemberRemove', raw);
280
+ const member = raw;
281
+ const guildId = member.guild?.id;
282
+ const userId = member.user?.id;
283
+ if (!guildId || !userId)
284
+ return;
285
+ handlers.onGuildMemberRemove?.({
286
+ guildId,
287
+ userId,
288
+ userName: member.user?.username || member.user?.displayName,
289
+ });
290
+ }).catch(() => logger.debug('Native callback was not admitted'));
278
291
  });
279
292
  client.once('clientReady', () => {
280
293
  if (signal?.aborted)
@@ -334,3 +347,15 @@ export async function connectDiscordGatewayClient(client, config, handlers, sign
334
347
  });
335
348
  });
336
349
  }
350
+ /** Logical cancellation only; the native SDK still owns its already-started ACK. */
351
+ function awaitOwnedAck(operation, signal) {
352
+ if (!signal)
353
+ return operation;
354
+ return new Promise((resolve, reject) => {
355
+ const abort = () => reject(signal.reason);
356
+ signal.addEventListener('abort', abort, { once: true });
357
+ void operation.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
358
+ if (signal.aborted)
359
+ abort();
360
+ });
361
+ }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, type DiscordActivity, type DiscordEndpointConfig, type DiscordButtonInbound, type DiscordInboundAttachment, type DiscordInboundMessage, type DiscordOutboundBody, type DiscordWireSegment, type ResolvedDiscordConfig, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig, } from './protocol.js';
2
- export { discordClient, requireDiscordGatewayClient, type DiscordClient, type DiscordClientEventMap, } from './client.js';
2
+ export { requireDiscordGatewayClient, type DiscordClient, type DiscordClientEventMap, } from './client.js';
3
3
  export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
4
- export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type CreateDiscordClient, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
5
- export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
4
+ export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
5
+ export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
6
6
  export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, type DiscordInteractionsHandler, } from './webhook.js';
7
+ export { DiscordAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, } from './protocol.js';
2
- export { discordClient, requireDiscordGatewayClient, } from './client.js';
2
+ export { requireDiscordGatewayClient, } from './client.js';
3
3
  export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
4
4
  export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from './endpoint.js';
5
- export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
5
+ export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
6
6
  export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, } from './webhook.js';
7
+ export { DiscordAdapter } from './adapter.js';
package/lib/protocol.d.ts CHANGED
@@ -56,6 +56,8 @@ export interface DiscordInboundAttachment {
56
56
  readonly size?: number;
57
57
  }
58
58
  export interface DiscordInboundMessage {
59
+ readonly event?: unknown;
60
+ readonly platformSentAt?: number;
59
61
  readonly id: string;
60
62
  readonly content: string;
61
63
  readonly channelId: string;
@@ -75,6 +77,8 @@ export interface DiscordInboundMessage {
75
77
  readonly mentionedBot?: boolean;
76
78
  }
77
79
  export interface DiscordButtonInbound {
80
+ readonly event?: unknown;
81
+ readonly platformSentAt?: number;
78
82
  readonly guildId?: string;
79
83
  readonly id: string;
80
84
  readonly customId: string;
package/lib/protocol.js CHANGED
@@ -330,6 +330,7 @@ export function interactionToInboundMessage(interaction) {
330
330
  const user = interaction.member?.user
331
331
  ?? interaction.user;
332
332
  return {
333
+ event: interaction,
333
334
  id: String(interaction.id),
334
335
  content: formatSlashCommandContent(interaction),
335
336
  channelId: String(interaction.channel_id ?? ''),
package/lib/webhook.d.ts CHANGED
@@ -1,15 +1,14 @@
1
+ import { type Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * Discord interactions HTTP: signature → parse → admit.
3
4
  */
4
5
  import type { IncomingMessage, ServerResponse } from 'node:http';
5
- import { type Locale } from 'zhin.js/i18n';
6
6
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
7
7
  import { type DiscordInboundMessage, type DiscordButtonInbound, type ResolvedDiscordInteractionsConfig } from './protocol.js';
8
- export interface DiscordInteractionsHandler {
8
+ export interface DiscordInteractionsHandler extends Endpoint {
9
9
  readonly config: ResolvedDiscordInteractionsConfig;
10
10
  readonly isOpen: boolean;
11
11
  admit(msg: DiscordInboundMessage): void;
12
- resolveLocale?(senderId: string): Promise<Locale | undefined>;
13
12
  admitButton(interaction: DiscordButtonInbound): void;
14
13
  admitPlatform(event: Record<string, unknown>): void;
15
14
  }
package/lib/webhook.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { endpointSpec } from 'zhin.js/adapter';
1
2
  import { createMessages } from 'zhin.js/i18n';
2
3
  import { getLogger } from '@zhin.js/logger';
3
4
  import { interactionToInboundMessage, verifyDiscordInteractionSignature, } from './protocol.js';
@@ -35,45 +36,56 @@ export async function handleDiscordInteractionRequest(request, response, handler
35
36
  return;
36
37
  }
37
38
  const interaction = JSON.parse(rawBody);
38
- if (handler.isOpen)
39
- handler.admitPlatform(interaction);
39
+ if (!interaction || interaction.application_id !== handler.config.applicationId) {
40
+ response.writeHead(401, { 'Content-Type': 'text/plain' });
41
+ response.end('Unauthorized');
42
+ return;
43
+ }
44
+ // A verified protocol challenge is not a business message and keeps its immediate ACK.
40
45
  if (interaction.type === INTERACTION_TYPE_PING) {
46
+ if (handler.isOpen)
47
+ void handler[endpointSpec].events.run(() => handler.admitPlatform(interaction))
48
+ .catch(() => logger.debug('Protocol event dispatch did not complete'));
41
49
  writeJson(response, 200, { type: INTERACTION_RESPONSE_PONG });
42
50
  return;
43
51
  }
44
- if (interaction.type === 3) {
45
- const data = interaction.data;
46
- if (data?.component_type === 2 && typeof data.custom_id === 'string') {
47
- if (!handler.isOpen) {
48
- response.writeHead(503);
49
- response.end('Unavailable');
52
+ await handler[endpointSpec].events.run(async () => {
53
+ if (handler.isOpen)
54
+ handler.admitPlatform(interaction);
55
+ if (interaction.type === 3) {
56
+ const data = interaction.data;
57
+ if (data?.component_type === 2 && typeof data.custom_id === 'string') {
58
+ if (!handler.isOpen) {
59
+ response.writeHead(503);
60
+ response.end('Unavailable');
61
+ return;
62
+ }
63
+ const message = interactionToInboundMessage(interaction);
64
+ handler.admitButton({ event: interaction, id: message.id, customId: data.custom_id, channelId: message.channelId, channelKind: message.channelKind, userId: message.authorId, userName: message.authorName, ...(message.guildId ? { guildId: message.guildId } : {}), sourceMessageId: interaction.message?.id });
65
+ writeJson(response, 200, { type: 6 });
50
66
  return;
51
67
  }
68
+ }
69
+ if (interaction.type === INTERACTION_TYPE_APPLICATION_COMMAND) {
52
70
  const message = interactionToInboundMessage(interaction);
53
- handler.admitButton({ id: message.id, customId: data.custom_id, channelId: message.channelId, channelKind: message.channelKind, userId: message.authorId, userName: message.authorName, ...(message.guildId ? { guildId: message.guildId } : {}), sourceMessageId: interaction.message?.id });
54
- writeJson(response, 200, { type: 6 });
71
+ if (handler.isOpen) {
72
+ handler.admit(message);
73
+ }
74
+ const locale = await handler[endpointSpec].resolveLocale(message.authorId);
75
+ // 即时响应(type 4):defer(type 5) 需要 followup PATCH,未实现会让用户端一直转圈
76
+ const commandName = String(interaction.data?.name ?? '');
77
+ writeJson(response, 200, {
78
+ type: INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
79
+ data: {
80
+ content: messages(locale, 'processing', { command: commandName }),
81
+ flags: INTERACTION_FLAG_EPHEMERAL,
82
+ },
83
+ });
55
84
  return;
56
85
  }
57
- }
58
- if (interaction.type === INTERACTION_TYPE_APPLICATION_COMMAND) {
59
- const message = interactionToInboundMessage(interaction);
60
- if (handler.isOpen) {
61
- handler.admit(message);
62
- }
63
- const locale = await handler.resolveLocale?.(message.authorId);
64
- // 即时响应(type 4):defer(type 5) 需要 followup PATCH,未实现会让用户端一直转圈
65
- const commandName = String(interaction.data?.name ?? '');
66
- writeJson(response, 200, {
67
- type: INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
68
- data: {
69
- content: messages(locale, 'processing', { command: commandName }),
70
- flags: INTERACTION_FLAG_EPHEMERAL,
71
- },
72
- });
73
- return;
74
- }
75
- response.writeHead(400, { 'Content-Type': 'text/plain' });
76
- response.end('Unsupported interaction type');
86
+ response.writeHead(400, { 'Content-Type': 'text/plain' });
87
+ response.end('Unsupported interaction type');
88
+ });
77
89
  }
78
90
  catch (error) {
79
91
  logger.error('Discord interactions error:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-discord",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "Zhin.js Discord adapter for Plugin Runtime (Gateway WebSocket)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,26 +35,26 @@
35
35
  "@discordjs/ws": "1.2.3",
36
36
  "discord.js": "^14.27.0",
37
37
  "undici": "6.29.1",
38
- "@zhin.js/adapter": "1.1.25",
39
- "@zhin.js/agent-feature": "1.1.10",
40
- "@zhin.js/client": "1.1.21",
38
+ "@zhin.js/adapter": "1.1.26",
39
+ "@zhin.js/agent-feature": "1.1.11",
40
+ "@zhin.js/client": "1.1.22",
41
41
  "@zhin.js/console-contract": "1.1.3",
42
- "@zhin.js/core": "1.1.53",
43
- "@zhin.js/feature-kit": "1.1.9",
44
- "@zhin.js/host-http": "1.1.11",
42
+ "@zhin.js/core": "1.1.54",
43
+ "@zhin.js/feature-kit": "1.1.10",
44
+ "@zhin.js/host-http": "1.1.12",
45
45
  "@zhin.js/im-contract": "1.1.9",
46
46
  "@zhin.js/logger": "1.1.2",
47
- "@zhin.js/page": "1.1.10",
48
- "@zhin.js/permission": "1.1.9",
49
- "@zhin.js/skill": "1.1.10",
50
- "@zhin.js/tool": "1.1.12"
47
+ "@zhin.js/page": "1.1.11",
48
+ "@zhin.js/permission": "1.1.10",
49
+ "@zhin.js/skill": "1.1.11",
50
+ "@zhin.js/tool": "1.1.13"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "zod": "^4.0.0",
54
- "@zhin.js/adapter": "^1.1.25",
55
- "@zhin.js/command": "^1.1.11",
56
- "@zhin.js/core": "^1.1.53",
57
- "zhin.js": "^1.1.18"
54
+ "@zhin.js/adapter": "^1.1.26",
55
+ "@zhin.js/command": "^1.1.12",
56
+ "@zhin.js/core": "^1.1.54",
57
+ "zhin.js": "^1.1.19"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "@zhin.js/command": {
@@ -72,8 +72,8 @@
72
72
  "typescript": "^6.0.3",
73
73
  "vitest": "^4.1.10",
74
74
  "zod": "^4.4.3",
75
- "@zhin.js/host-http": "1.1.11",
76
- "zhin.js": "1.1.18"
75
+ "@zhin.js/host-http": "1.1.12",
76
+ "zhin.js": "1.1.19"
77
77
  },
78
78
  "files": [
79
79
  "adapters",
@@ -104,7 +104,7 @@
104
104
  "features": [
105
105
  {
106
106
  "package": "@zhin.js/adapter",
107
- "api": "^1.0.0"
107
+ "api": "^2.0.0"
108
108
  },
109
109
  {
110
110
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
3
  import { permissionHostToken } from '@zhin.js/permission';
5
4
  import { checkDiscordPlatformPermit } from "./lib/platform-permit.js";
6
- import { discordRuntimeStateToken } from "./lib/discord-runtime-state.js";
7
5
  import { createDiscordConsoleRpc } from "./lib/console.js";
8
6
  export default definePlugin({
9
7
  name: 'discord',
@@ -11,8 +9,7 @@ export default definePlugin({
11
9
  displayName: 'Discord Gateway Adapter',
12
10
  },
13
11
  setup(context) {
14
- context.resources.provide(discordRuntimeStateToken, createEndpointRuntimeState());
15
- context.resources.provide(pluginConsoleRpcToken, createDiscordConsoleRpc(context.resources, () => context.config.get()));
12
+ context.resources.provide(pluginConsoleRpcToken, createDiscordConsoleRpc(() => context.config.get()));
16
13
  if (context.resources.has(permissionHostToken)) {
17
14
  const host = context.resources.use(permissionHostToken);
18
15
  return host.registerPlatform('discord', checkDiscordPlatformPermit);
package/src/adapter.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { httpHostToken } from '@zhin.js/host-http';
3
+ import { REST, Routes } from 'discord.js';
4
+ import { DiscordGatewayEndpoint, DiscordInteractionsEndpoint } from './endpoint.js';
5
+ import { createDiscordRestApiAgent } from './rest-api-proxy.js';
6
+ import { resolveDiscordConfig, type DiscordEndpointConfig } from './protocol.js';
7
+ import type { DiscordClientEventMap } from './client.js';
8
+
9
+ export class DiscordAdapter extends Adapter<DiscordGatewayEndpoint | DiscordInteractionsEndpoint, DiscordEndpointConfig, DiscordClientEventMap> {
10
+ constructor(private readonly context: AdapterContext<DiscordEndpointConfig>) { super(); }
11
+ override get configurationIdentityFields(): readonly string[] { return ['id', 'applicationId']; }
12
+ override get configurationIdentityProbeFields(): readonly string[] { return ['token', 'publicKey']; }
13
+ override get configurationUpdateFields(): readonly string[] { return ['token', 'connection', 'restApiProxy', 'restApiProxy.port',
14
+ 'gatewayFaultProxyUrl', 'intents', 'defaultActivity', 'defaultActivity.name', 'defaultActivity.type', 'defaultActivity.url', 'publicKey', 'interactionsPath']; }
15
+ protected override validateConfiguration(input: DiscordEndpointConfig): void { resolveDiscordConfig(input); }
16
+ protected override async identifyConfiguration(input: DiscordEndpointConfig, signal: AbortSignal): Promise<string> {
17
+ const config = resolveDiscordConfig(input);
18
+ const agent = config.connection === 'gateway' && config.restApiProxy ? createDiscordRestApiAgent(config.restApiProxy) : undefined;
19
+ const rest = new REST({ version: '10', retries: 0, rejectOnRateLimit: () => true, ...(agent ? { agent } : {}) }).setToken(config.token);
20
+ try {
21
+ const application = await rest.get(Routes.currentApplication(), { signal }) as { id?: unknown; verify_key?: unknown };
22
+ if (typeof application?.id !== 'string' || !application.id.trim()) throw new Error('Discord identity response is incomplete');
23
+ if (config.connection === 'interactions' && application.id !== config.applicationId) throw new Error('Discord application identity configuration does not match');
24
+ if (config.connection === 'interactions' && (typeof application.verify_key !== 'string'
25
+ || application.verify_key.toLowerCase() !== config.publicKey.toLowerCase())) {
26
+ throw new Error('Discord verification key does not match the application identity');
27
+ }
28
+ return JSON.stringify(['discord', application.id]);
29
+ } finally { rest.clearHashSweeper(); rest.clearHandlerSweeper(); await agent?.destroyProxy(); }
30
+ }
31
+ protected async createEndpoint(input: DiscordEndpointConfig, execution: EndpointCreationContext): Promise<DiscordGatewayEndpoint | DiscordInteractionsEndpoint> {
32
+ const config = resolveDiscordConfig(input);
33
+ if (config.connection === 'interactions') return new DiscordInteractionsEndpoint({ id: execution.canonicalId, config, http: this.context.use(httpHostToken) });
34
+ const restApiAgent = config.restApiProxy ? createDiscordRestApiAgent(config.restApiProxy) : undefined;
35
+ try { return new DiscordGatewayEndpoint({ id: execution.canonicalId, config, restApiAgent }); }
36
+ catch (error) { await restApiAgent?.destroyProxy(); throw error; }
37
+ }
38
+ }