@zhin.js/adapter-discord 8.0.0 → 8.0.2

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/src/endpoint.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
3
4
  */
@@ -8,11 +9,9 @@ import type {
8
9
  EndpointContentResolveContext,
9
10
  EndpointControl,
10
11
  EndpointGroup,
11
- EndpointInstance,
12
12
  EndpointManagement,
13
13
  EndpointSendRequest,
14
14
  } from 'zhin.js/adapter';
15
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
16
15
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
17
16
  import {
18
17
  type ConversationReference,
@@ -21,7 +20,6 @@ import {
21
20
  } from '@zhin.js/im-contract';
22
21
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
23
22
  import type { CapabilityId } from 'zhin.js';
24
- import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
25
23
  import {
26
24
  connectDiscordGatewayClient,
27
25
  defaultCreateClient,
@@ -58,14 +56,12 @@ export type {
58
56
 
59
57
  export interface DiscordEndpointOptions {
60
58
  readonly id: CapabilityId;
61
- readonly gateway: MessageGateway;
62
- readonly sideEvents?: SideEventGateway;
63
59
  readonly config: ResolvedDiscordGatewayConfig;
64
60
  readonly createClient?: CreateDiscordClient;
65
61
  readonly fetch?: typeof globalThis.fetch;
66
62
  }
67
63
 
68
- export class DiscordGatewayEndpoint implements EndpointInstance {
64
+ export class DiscordGatewayEndpoint extends Endpoint<DiscordClientTransport> {
69
65
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
70
66
 
71
67
  readonly #options: DiscordEndpointOptions;
@@ -74,11 +70,9 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
74
70
  #client: DiscordClientTransport | null = null;
75
71
  #open = false;
76
72
  #started = false;
77
- #unregisterAgent?: () => void;
78
- readonly management: EndpointManagement = createDiscordEndpointManagement({
79
- getClient: () => this.#requireClient(),
80
- getMembers: (guildId) => this.getMembers(guildId),
81
- });
73
+ readonly management: EndpointManagement = createDiscordEndpointManagement(
74
+ () => this.#requireClient(),
75
+ );
82
76
  readonly control: EndpointControl = Object.freeze({
83
77
  recall: (message: MessageRef) => this.recallMessage(message),
84
78
  addReaction: async (
@@ -88,7 +82,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
88
82
  ) => {
89
83
  const channelId = hint?.channelId ?? message.conversation.id;
90
84
  if (!channelId || !message.id) return null;
91
- await this.addReaction(channelId, message.id, emoji);
85
+ await this.#addReaction(channelId, message.id, emoji);
92
86
  return emoji;
93
87
  },
94
88
  });
@@ -98,27 +92,35 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
98
92
  });
99
93
 
100
94
  constructor(options: DiscordEndpointOptions) {
95
+ super();
101
96
  this.#logger = getAdapterLogger('discord', options.config.id);
102
97
  this.#options = options;
103
98
  this.#createClient = options.createClient ?? defaultCreateClient;
104
99
  this.#fetch = options.fetch ?? globalThis.fetch;
105
100
  }
106
101
 
102
+ /** The actual discord.js-compatible client used by this connection. */
103
+ get client(): DiscordClientTransport {
104
+ return this.#requireClient();
105
+ }
106
+
107
107
  async start(): Promise<void> {
108
108
  if (this.#started) return;
109
109
  this.#started = true;
110
110
  try {
111
- this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.id, this);
112
111
  const intents = this.#options.config.intents?.length
113
112
  ? [...this.#options.config.intents]
114
113
  : DEFAULT_INTENTS;
115
114
  this.#client = this.#createClient(intents);
116
115
  await connectDiscordGatewayClient(this.#client, this.#options.config, {
116
+ onPlatformEvent: (name, event) => {
117
+ void this.#emitPlatformEvent(name, event);
118
+ },
117
119
  onMessage: (msg) => this.admit(msg),
118
120
  onButton: (interaction) => this.admitButton(interaction),
119
121
  onGuildMemberAdd: (member) => {
120
122
  receiveDiscordGuildMemberSideEvent(
121
- this.#options.sideEvents,
123
+ (name, payload) => this.emit(name, payload),
122
124
  this.#options.config.id,
123
125
  'member_increase',
124
126
  member,
@@ -127,7 +129,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
127
129
  },
128
130
  onGuildMemberRemove: (member) => {
129
131
  receiveDiscordGuildMemberSideEvent(
130
- this.#options.sideEvents,
132
+ (name, payload) => this.emit(name, payload),
131
133
  this.#options.config.id,
132
134
  'member_decrease',
133
135
  member,
@@ -158,8 +160,6 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
158
160
 
159
161
  async stop(): Promise<void> {
160
162
  this.#open = false;
161
- this.#unregisterAgent?.();
162
- this.#unregisterAgent = undefined;
163
163
  if (this.#client) {
164
164
  try {
165
165
  this.#client.removeAllListeners();
@@ -198,7 +198,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
198
198
  if (!this.#open) return;
199
199
  if (msg.authorBot) return;
200
200
  const conversation = discordInboundConversation(String(this.#options.id), msg);
201
- void this.#options.gateway.receive({
201
+ void this.emit('message.receive', {
202
202
  conversation,
203
203
  message: { conversation, id: msg.id },
204
204
  content: formatInboundContent(msg),
@@ -230,7 +230,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
230
230
  admitButton(interaction: DiscordButtonInbound): void {
231
231
  if (!this.#open) return;
232
232
  const conversation = discordInboundConversation(String(this.#options.id), interaction);
233
- void this.#options.gateway.receive({
233
+ void this.emit('message.receive', {
234
234
  conversation,
235
235
  message: { conversation, id: interaction.id },
236
236
  content: formatButtonContent(interaction),
@@ -251,58 +251,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
251
251
  });
252
252
  }
253
253
 
254
- // ── Agent tool surface ──────────────────────────────────────────────
255
-
256
- async addRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
257
- const member = await this.#fetchMember(guildId, userId) as { roles: { add(id: string): Promise<unknown> } };
258
- await member.roles.add(roleId);
259
- return true;
260
- }
261
-
262
- async removeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
263
- const member = await this.#fetchMember(guildId, userId) as { roles: { remove(id: string): Promise<unknown> } };
264
- await member.roles.remove(roleId);
265
- return true;
266
- }
267
-
268
- async getRoles(guildId: string): Promise<unknown[]> {
269
- const guild = await this.#requireClient().guilds.fetch(guildId);
270
- await guild.roles.fetch();
271
- const cache = guild.roles.cache as Map<string, {
272
- id: string;
273
- name: string;
274
- hexColor: string;
275
- position: number;
276
- permissions: { bitfield: bigint };
277
- }>;
278
- return [...cache.values()].map((role) => ({
279
- id: role.id,
280
- name: role.name,
281
- color: role.hexColor,
282
- position: role.position,
283
- permissions: role.permissions.bitfield.toString(),
284
- }));
285
- }
286
-
287
- async createThread(
288
- channelId: string,
289
- name: string,
290
- messageId?: string,
291
- autoArchiveDuration?: number,
292
- ): Promise<{ id: string }> {
293
- const channel = await this.#requireClient().channels.fetch(channelId);
294
- if (!channel || !('threads' in channel) || !channel.threads) {
295
- throw new Error(`Channel ${channelId} 不支持创建帖子`);
296
- }
297
- const options: Record<string, unknown> = {
298
- name,
299
- autoArchiveDuration: autoArchiveDuration || 1440,
300
- };
301
- if (messageId) options.startMessage = messageId;
302
- return channel.threads.create(options);
303
- }
304
-
305
- async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
254
+ async #addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
306
255
  const channel = await this.#requireClient().channels.fetch(channelId);
307
256
  if (!channel?.isTextBased() || !channel.messages) {
308
257
  throw new Error(`Channel ${channelId} 不是文本频道`);
@@ -311,105 +260,14 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
311
260
  await message.react(emoji);
312
261
  }
313
262
 
314
- async sendEmbed(
315
- channelId: string,
316
- embedData: Record<string, unknown>,
317
- ): Promise<{ id: string }> {
318
- const body: DiscordOutboundBody = { embeds: [embedData] };
319
- const id = await this.#sendBody(channelId, body);
320
- return { id };
321
- }
322
-
323
- async createForumPost(
324
- channelId: string,
325
- name: string,
326
- content: string,
327
- tags?: string[],
328
- ): Promise<{ id: string }> {
329
- const channel = await this.#requireClient().channels.fetch(channelId);
330
- if (!channel || channel.type !== ChannelType.GuildForum || !channel.threads) {
331
- throw new Error(`Channel ${channelId} 不是论坛频道`);
332
- }
333
- const options: Record<string, unknown> = {
334
- name,
335
- message: { content },
336
- };
337
- if (tags?.length && channel.availableTags?.length) {
338
- const tagIds = channel.availableTags
339
- .filter((t) => tags.includes(t.name))
340
- .map((t) => t.id);
341
- if (tagIds.length) options.appliedTags = tagIds;
342
- }
343
- return channel.threads.create(options);
344
- }
345
-
346
- async kickMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
347
- const member = await this.#fetchMember(guildId, userId) as { kick(reason?: string): Promise<unknown> };
348
- await member.kick(reason);
349
- return true;
350
- }
351
-
352
- async banMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
353
- const guild = await this.#requireClient().guilds.fetch(guildId);
354
- await guild.members.ban(userId, { reason });
355
- return true;
356
- }
357
-
358
- async unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
359
- const guild = await this.#requireClient().guilds.fetch(guildId);
360
- await guild.members.unban(userId, reason);
361
- return true;
362
- }
363
-
364
- async timeoutMember(
365
- guildId: string,
366
- userId: string,
367
- duration = 600,
368
- reason?: string,
369
- ): Promise<boolean> {
370
- const member = await this.#fetchMember(guildId, userId) as {
371
- timeout(ms: number | null, reason?: string): Promise<unknown>;
372
- };
373
- await member.timeout(duration === 0 ? null : duration * 1000, reason);
374
- return true;
375
- }
376
-
377
- async setNickname(guildId: string, userId: string, nickname: string): Promise<boolean> {
378
- const member = await this.#fetchMember(guildId, userId) as {
379
- setNickname(nickname: string): Promise<unknown>;
380
- };
381
- await member.setNickname(nickname);
382
- return true;
383
- }
384
-
385
- async getMembers(guildId: string, limit = 100): Promise<unknown[]> {
386
- const guild = await this.#requireClient().guilds.fetch(guildId);
387
- const members = await guild.members.fetch({ limit }) as Map<string, {
388
- id: string;
389
- user: { username: string };
390
- nickname: string | null;
391
- roles: { cache: { map(fn: (r: { id: string }) => string): string[] } };
392
- joinedAt?: Date | null;
393
- }>;
394
- return [...members.values()].map((member) => ({
395
- id: member.id,
396
- username: member.user.username,
397
- nickname: member.nickname,
398
- roles: member.roles.cache.map((r) => r.id),
399
- joined_at: member.joinedAt?.toISOString(),
400
- }));
401
- }
402
-
403
- async getGuildInfo(guildId: string): Promise<unknown> {
404
- const guild = await this.#requireClient().guilds.fetch(guildId);
405
- return {
406
- id: guild.id,
407
- name: guild.name,
408
- icon: guild.iconURL?.(),
409
- owner_id: guild.ownerId,
410
- member_count: guild.memberCount,
411
- created_at: guild.createdAt?.toISOString(),
412
- };
263
+ async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
264
+ await this.emitPlatform(name, event).catch((error) => {
265
+ this.#logger.warn(formatCompact({
266
+ op: 'discord_platform_event_failed',
267
+ event: name,
268
+ error: error instanceof Error ? error.message : String(error),
269
+ }));
270
+ });
413
271
  }
414
272
 
415
273
  async #sendBody(channelId: string, body: DiscordOutboundBody): Promise<string> {
@@ -422,11 +280,6 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
422
280
  return result.id;
423
281
  }
424
282
 
425
- async #fetchMember(guildId: string, userId: string): Promise<unknown> {
426
- const guild = await this.#requireClient().guilds.fetch(guildId);
427
- return guild.members.fetch(userId);
428
- }
429
-
430
283
  #requireClient(): DiscordClientTransport {
431
284
  if (!this.#client) throw new Error('Discord client not connected');
432
285
  return this.#client;
@@ -435,14 +288,50 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
435
288
 
436
289
  export interface DiscordInteractionsEndpointOptions {
437
290
  readonly id: CapabilityId;
438
- readonly gateway: MessageGateway;
439
- readonly sideEvents?: SideEventGateway;
440
291
  readonly http: HttpHost;
441
292
  readonly config: ResolvedDiscordInteractionsConfig;
442
293
  readonly fetch?: typeof globalThis.fetch;
443
294
  }
444
295
 
445
- export class DiscordInteractionsEndpoint implements EndpointInstance {
296
+ /** Minimal Discord REST client used when Gateway is intentionally disabled. */
297
+ export class DiscordRestClient {
298
+ constructor(
299
+ readonly token: string,
300
+ readonly fetch: typeof globalThis.fetch = globalThis.fetch,
301
+ ) {}
302
+
303
+ async request<T = unknown>(
304
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
305
+ path: string,
306
+ body?: unknown,
307
+ ): Promise<T> {
308
+ const response = await this.fetch(`${DISCORD_API}${path}`, {
309
+ method,
310
+ headers: {
311
+ Authorization: `Bot ${this.token}`,
312
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
313
+ },
314
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
315
+ signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
316
+ });
317
+ const text = await response.text();
318
+ if (!response.ok) {
319
+ throw new Error(`Discord API ${method} ${path} failed (${response.status}): ${text.slice(0, 200)}`);
320
+ }
321
+ return (text ? JSON.parse(text) : undefined) as T;
322
+ }
323
+
324
+ createMessage(channelId: string, body: unknown): Promise<{ id?: string }> {
325
+ return this.request('POST', `/channels/${channelId}/messages`, body);
326
+ }
327
+
328
+ deleteMessage(channelId: string, messageId: string): Promise<void> {
329
+ return this.request('DELETE', `/channels/${channelId}/messages/${messageId}`);
330
+ }
331
+ }
332
+
333
+ export class DiscordInteractionsEndpoint extends Endpoint<DiscordRestClient> {
334
+ readonly client: DiscordRestClient;
446
335
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
447
336
 
448
337
  readonly #options: DiscordInteractionsEndpointOptions;
@@ -459,9 +348,11 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
459
348
  });
460
349
 
461
350
  constructor(options: DiscordInteractionsEndpointOptions) {
351
+ super();
462
352
  this.#logger = getAdapterLogger('discord', options.config.id);
463
353
  this.#options = options;
464
354
  this.#fetch = options.fetch ?? globalThis.fetch;
355
+ this.client = new DiscordRestClient(options.config.token, this.#fetch);
465
356
  }
466
357
 
467
358
  get isOpen(): boolean {
@@ -501,42 +392,19 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
501
392
 
502
393
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
503
394
  const body = formatOutboundBody(payload);
504
- const channelId = conversation.id;
505
- const response = await this.#fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
506
- method: 'POST',
507
- headers: {
508
- Authorization: `Bot ${this.#options.config.token}`,
509
- 'Content-Type': 'application/json',
510
- },
511
- body: JSON.stringify(body),
512
- signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
513
- });
514
- const text = await response.text();
515
- if (!response.ok) {
516
- throw new Error(`Discord send failed (${response.status}): ${text.slice(0, 200)}`);
517
- }
518
- const data = JSON.parse(text) as { id?: string };
519
- const snowflake = data.id ?? '';
520
- return snowflake;
395
+ const data = await this.client.createMessage(conversation.id, body);
396
+ return data.id ?? '';
521
397
  }
522
398
 
523
399
  async recallMessage(message: MessageRef): Promise<void> {
524
400
  if (!message.id) return;
525
- const response = await this.#fetch(`${DISCORD_API}/channels/${message.conversation.id}/messages/${message.id}`, {
526
- method: 'DELETE',
527
- headers: { Authorization: `Bot ${this.#options.config.token}` },
528
- signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
529
- });
530
- if (!response.ok && response.status !== 404) {
531
- const text = await response.text();
532
- throw new Error(`Discord recall failed (${response.status}): ${text.slice(0, 200)}`);
533
- }
401
+ await this.client.deleteMessage(message.conversation.id, message.id);
534
402
  }
535
403
 
536
404
  admit(msg: DiscordInboundMessage): void {
537
405
  if (!this.#open) return;
538
406
  const conversation = discordInboundConversation(String(this.#options.id), msg);
539
- void this.#options.gateway.receive({
407
+ void this.emit('message.receive', {
540
408
  conversation,
541
409
  message: { conversation, id: msg.id },
542
410
  content: formatInboundContent(msg),
@@ -561,6 +429,18 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
561
429
  }));
562
430
  });
563
431
  }
432
+
433
+ admitPlatform(event: Record<string, unknown>): void {
434
+ if (!this.#open) return;
435
+ const type = typeof event.type === 'number' ? `interaction.${event.type}` : 'interaction';
436
+ void this.emitPlatform(type, event).catch((error) => {
437
+ this.#logger.warn(formatCompact({
438
+ op: 'discord_platform_event_failed',
439
+ event: type,
440
+ error: error instanceof Error ? error.message : String(error),
441
+ }));
442
+ });
443
+ }
564
444
  }
565
445
 
566
446
  async function resolveDiscordContent(
@@ -628,14 +508,13 @@ function toGroupId(id: string): number {
628
508
  * DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
629
509
  * 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
630
510
  */
631
- export function createDiscordEndpointManagement(endpoint: {
632
- getClient(): DiscordClientTransport;
633
- getMembers(guildId: string): Promise<unknown[]>;
634
- }): EndpointManagement {
511
+ export function createDiscordEndpointManagement(
512
+ requireClient: () => DiscordClientTransport,
513
+ ): EndpointManagement {
635
514
  return Object.freeze<EndpointManagement>({
636
515
  async listGroups(): Promise<readonly EndpointGroup[]> {
637
516
  const groups: EndpointGroup[] = [];
638
- for (const guild of endpoint.getClient().guilds.cache.values()) {
517
+ for (const guild of requireClient().guilds.cache.values()) {
639
518
  if (!guild?.id) continue;
640
519
  groups.push({
641
520
  group_id: toGroupId(String(guild.id)),
@@ -646,7 +525,7 @@ export function createDiscordEndpointManagement(endpoint: {
646
525
  },
647
526
  async listChannels(): Promise<readonly EndpointChannel[]> {
648
527
  const channels: EndpointChannel[] = [];
649
- for (const guild of endpoint.getClient().guilds.cache.values()) {
528
+ for (const guild of requireClient().guilds.cache.values()) {
650
529
  if (!guild?.id) continue;
651
530
  const guildId = String(guild.id);
652
531
  const guildName = String(guild.name ?? guildId);
@@ -663,7 +542,21 @@ export function createDiscordEndpointManagement(endpoint: {
663
542
  return channels;
664
543
  },
665
544
  async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
666
- return endpoint.getMembers(groupId);
545
+ const guild = await requireClient().guilds.fetch(groupId);
546
+ const members = await guild.members.fetch({ limit: 100 }) as Map<string, {
547
+ id: string;
548
+ user: { username: string };
549
+ nickname: string | null;
550
+ roles: { cache: { map(fn: (role: { id: string }) => string): string[] } };
551
+ joinedAt?: Date | null;
552
+ }>;
553
+ return [...members.values()].map((member) => ({
554
+ id: member.id,
555
+ username: member.user.username,
556
+ nickname: member.nickname,
557
+ roles: member.roles.cache.map((role) => role.id),
558
+ joined_at: member.joinedAt?.toISOString(),
559
+ }));
667
560
  },
668
561
  });
669
562
  }
package/src/gateway.ts CHANGED
@@ -285,6 +285,7 @@ function decodeBase64(value: string): Buffer {
285
285
  }
286
286
 
287
287
  export interface DiscordGatewayConnectHandlers {
288
+ onPlatformEvent(name: string, event: unknown): void;
288
289
  onMessage(msg: DiscordInboundMessage): void;
289
290
  onButton(interaction: DiscordButtonInbound): void;
290
291
  onGuildMemberAdd?(member: { guildId: string; userId: string; userName?: string }): void;
@@ -300,6 +301,7 @@ export async function connectDiscordGatewayClient(
300
301
  let settled = false;
301
302
 
302
303
  client.on('messageCreate', (raw) => {
304
+ handlers.onPlatformEvent('messageCreate', raw);
303
305
  const msg = normalizeDiscordMessage(raw);
304
306
  if (!msg) return;
305
307
  // clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
@@ -310,6 +312,7 @@ export async function connectDiscordGatewayClient(
310
312
  });
311
313
 
312
314
  client.on('interactionCreate', (raw) => {
315
+ handlers.onPlatformEvent('interactionCreate', raw);
313
316
  const interaction = raw as {
314
317
  isButton?(): boolean;
315
318
  deferUpdate?(): Promise<unknown>;
@@ -334,6 +337,7 @@ export async function connectDiscordGatewayClient(
334
337
  });
335
338
 
336
339
  client.on('guildMemberAdd', (raw) => {
340
+ handlers.onPlatformEvent('guildMemberAdd', raw);
337
341
  const member = raw as {
338
342
  guild?: { id?: string };
339
343
  user?: { id?: string; username?: string; displayName?: string };
@@ -349,6 +353,7 @@ export async function connectDiscordGatewayClient(
349
353
  });
350
354
 
351
355
  client.on('guildMemberRemove', (raw) => {
356
+ handlers.onPlatformEvent('guildMemberRemove', raw);
352
357
  const member = raw as {
353
358
  guild?: { id?: string };
354
359
  user?: { id?: string; username?: string; displayName?: string };
@@ -364,6 +369,7 @@ export async function connectDiscordGatewayClient(
364
369
  });
365
370
 
366
371
  client.once('clientReady', () => {
372
+ handlers.onPlatformEvent('clientReady', client.user);
367
373
  void (async () => {
368
374
  try {
369
375
  if (config.defaultActivity && client.user?.setActivity) {
package/src/index.ts CHANGED
@@ -18,12 +18,10 @@ export {
18
18
  } from './protocol.js';
19
19
 
20
20
  export {
21
- getDiscordAgentDeps,
22
- registerDiscordAgentEndpoint,
23
- setDiscordAgentDeps,
24
- type DiscordAgentDeps,
25
- type DiscordAgentEndpoint,
26
- } from './discord-agent-deps.js';
21
+ discordClient,
22
+ type DiscordClient,
23
+ type DiscordClientEventMap,
24
+ } from './client.js';
27
25
 
28
26
  export {
29
27
  checkDiscordPlatformPermit,
package/src/protocol.ts CHANGED
@@ -227,7 +227,7 @@ export function senderDisplayName(msg: DiscordInboundMessage): string {
227
227
  return msg.authorName || msg.authorId;
228
228
  }
229
229
 
230
- /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
230
+ /** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
231
231
  export function formatInboundContent(msg: DiscordInboundMessage): string {
232
232
  const parts: string[] = [];
233
233
  if (msg.content?.trim()) parts.push(msg.content.trim());
@@ -1,5 +1,5 @@
1
1
  import { buildNotice, senderFromId } from '@zhin.js/core';
2
- import type { SideEventGateway } from '@zhin.js/core/runtime';
2
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
3
3
  import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
4
4
 
5
5
  export interface DiscordGuildMemberSideEvent {
@@ -9,14 +9,14 @@ export interface DiscordGuildMemberSideEvent {
9
9
  }
10
10
 
11
11
  export function receiveDiscordGuildMemberSideEvent(
12
- sideEvents: SideEventGateway | undefined,
12
+ emit: EndpointEventEmitter,
13
13
  configId: string,
14
14
  kind: 'member_increase' | 'member_decrease',
15
15
  event: DiscordGuildMemberSideEvent,
16
16
  logger: ReturnType<typeof getAdapterLogger>,
17
17
  ): void {
18
- if (!sideEvents) return;
19
- void sideEvents.receiveNotice(buildNotice(event, {
18
+ if (!emit) return;
19
+ void emit('notice.receive', buildNotice(event, {
20
20
  $id: `discord:guild_member:${kind}:${event.guildId}:${event.userId}:${Date.now()}`,
21
21
  $adapter: 'discord' as never,
22
22
  $endpoint: configId,
package/src/webhook.ts CHANGED
@@ -24,6 +24,7 @@ export interface DiscordInteractionsHandler {
24
24
  readonly config: ResolvedDiscordInteractionsConfig;
25
25
  readonly isOpen: boolean;
26
26
  admit(msg: DiscordInboundMessage): void;
27
+ admitPlatform(event: Record<string, unknown>): void;
27
28
  }
28
29
 
29
30
  export function registerDiscordInteractionRoutes(
@@ -63,6 +64,7 @@ export async function handleDiscordInteractionRequest(
63
64
  return;
64
65
  }
65
66
  const interaction = JSON.parse(rawBody) as Record<string, unknown>;
67
+ if (handler.isOpen) handler.admitPlatform(interaction);
66
68
  if (interaction.type === INTERACTION_TYPE_PING) {
67
69
  writeJson(response, 200, { type: INTERACTION_RESPONSE_PONG });
68
70
  return;
@@ -1,35 +0,0 @@
1
- /**
2
- * Agent tool deps for discord.
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- export interface DiscordAgentEndpoint {
6
- addRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
7
- removeRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
8
- getRoles(guildId: string): Promise<unknown[]>;
9
- createThread(channelId: string, name: string, messageId?: string, autoArchiveDuration?: number): Promise<{
10
- id: string;
11
- }>;
12
- addReaction(channelId: string, messageId: string, emoji: string): Promise<void>;
13
- sendEmbed(channelId: string, embedData: Record<string, unknown>): Promise<{
14
- id: string;
15
- }>;
16
- createForumPost(channelId: string, name: string, content: string, tags?: string[]): Promise<{
17
- id: string;
18
- }>;
19
- kickMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
20
- banMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
21
- unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
22
- timeoutMember(guildId: string, userId: string, duration?: number, reason?: string): Promise<boolean>;
23
- setNickname(guildId: string, userId: string, nickname: string): Promise<boolean>;
24
- getMembers(guildId: string, limit?: number): Promise<unknown[]>;
25
- getGuildInfo(guildId: string): Promise<unknown>;
26
- }
27
- export interface DiscordAgentDeps {
28
- getEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
29
- /** Alias kept for existing agent/tools that call getGatewayEndpoint. */
30
- getGatewayEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
31
- }
32
- export declare function registerDiscordAgentEndpoint(endpointKey: string, endpoint: DiscordAgentEndpoint): () => void;
33
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
34
- export declare function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void;
35
- export declare function getDiscordAgentDeps(): DiscordAgentDeps;
@@ -1,32 +0,0 @@
1
- /**
2
- * Agent tool deps for discord.
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- const endpoints = new Map();
6
- let override = null;
7
- export function registerDiscordAgentEndpoint(endpointKey, endpoint) {
8
- endpoints.set(endpointKey, endpoint);
9
- return () => {
10
- if (endpoints.get(endpointKey) === endpoint) {
11
- endpoints.delete(endpointKey);
12
- }
13
- };
14
- }
15
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
- export function setDiscordAgentDeps(deps) {
17
- override = deps;
18
- }
19
- function lookup(endpointKey) {
20
- const registered = endpoints.get(endpointKey);
21
- if (!registered)
22
- throw new Error(`Endpoint ${endpointKey} 不存在`);
23
- return registered;
24
- }
25
- export function getDiscordAgentDeps() {
26
- if (override)
27
- return override;
28
- return {
29
- getEndpoint: lookup,
30
- getGatewayEndpoint: lookup,
31
- };
32
- }