@zhin.js/adapter-discord 1.0.86 → 1.1.0

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 (80) hide show
  1. package/CHANGELOG.md +863 -65
  2. package/README.md +61 -67
  3. package/adapters/discord.js +41 -0
  4. package/adapters/discord.ts +57 -0
  5. package/agent/PERMITS.md +25 -0
  6. package/{skills/discord/SKILL.md → agent/skills/discord.md} +1 -1
  7. package/agent/tools/add_role.ts +26 -0
  8. package/agent/tools/create_thread.ts +28 -0
  9. package/agent/tools/forum_post.ts +33 -0
  10. package/agent/tools/list_roles.ts +34 -0
  11. package/agent/tools/react.ts +24 -0
  12. package/agent/tools/remove_role.ts +26 -0
  13. package/agent/tools/send_embed.ts +38 -0
  14. package/commands/endpoint/add/[id].js +3 -0
  15. package/commands/endpoint/add/[id].ts +3 -0
  16. package/commands/endpoint/list.js +3 -0
  17. package/commands/endpoint/list.ts +3 -0
  18. package/commands/endpoint/remove/[id].js +3 -0
  19. package/commands/endpoint/remove/[id].ts +3 -0
  20. package/lib/client.d.ts +16 -0
  21. package/lib/client.js +8 -0
  22. package/lib/discord-endpoint-commands.d.ts +1 -0
  23. package/lib/discord-endpoint-commands.js +18 -0
  24. package/lib/discord-runtime-state.d.ts +1 -0
  25. package/lib/discord-runtime-state.js +6 -0
  26. package/lib/endpoint.d.ts +72 -0
  27. package/lib/endpoint.js +458 -0
  28. package/lib/gateway.d.ts +144 -0
  29. package/lib/gateway.js +303 -0
  30. package/lib/index.d.ts +6 -18
  31. package/lib/index.js +6 -323
  32. package/lib/platform-permit.d.ts +14 -0
  33. package/lib/platform-permit.js +42 -0
  34. package/lib/protocol.d.ts +162 -0
  35. package/lib/protocol.js +333 -0
  36. package/lib/side-event-dispatch.d.ts +8 -0
  37. package/lib/side-event-dispatch.js +24 -0
  38. package/lib/webhook.d.ts +14 -0
  39. package/lib/webhook.js +88 -0
  40. package/package.json +62 -29
  41. package/plugin.js +19 -0
  42. package/schema.json +141 -0
  43. package/src/client.ts +24 -0
  44. package/src/discord-endpoint-commands.ts +19 -0
  45. package/src/discord-runtime-state.ts +7 -0
  46. package/src/endpoint.ts +562 -0
  47. package/src/gateway.ts +426 -0
  48. package/src/index.ts +55 -323
  49. package/src/platform-permit.ts +57 -0
  50. package/src/protocol.ts +502 -0
  51. package/src/side-event-dispatch.ts +37 -0
  52. package/src/webhook.ts +123 -0
  53. package/client/Dashboard.tsx +0 -195
  54. package/client/index.tsx +0 -11
  55. package/client/tsconfig.json +0 -7
  56. package/client/utils/api.ts +0 -17
  57. package/dist/index.js +0 -29
  58. package/lib/adapter.d.ts +0 -19
  59. package/lib/adapter.d.ts.map +0 -1
  60. package/lib/adapter.js +0 -90
  61. package/lib/adapter.js.map +0 -1
  62. package/lib/bot-interactions.d.ts +0 -33
  63. package/lib/bot-interactions.d.ts.map +0 -1
  64. package/lib/bot-interactions.js +0 -278
  65. package/lib/bot-interactions.js.map +0 -1
  66. package/lib/bot.d.ts +0 -117
  67. package/lib/bot.d.ts.map +0 -1
  68. package/lib/bot.js +0 -904
  69. package/lib/bot.js.map +0 -1
  70. package/lib/index.d.ts.map +0 -1
  71. package/lib/index.js.map +0 -1
  72. package/lib/types.d.ts +0 -49
  73. package/lib/types.d.ts.map +0 -1
  74. package/lib/types.js +0 -2
  75. package/lib/types.js.map +0 -1
  76. package/plugin.yml +0 -3
  77. package/src/adapter.ts +0 -107
  78. package/src/bot-interactions.ts +0 -348
  79. package/src/bot.ts +0 -1037
  80. package/src/types.ts +0 -60
@@ -0,0 +1,72 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ import type { EndpointContentPort, EndpointControl, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
3
+ import type { HttpHost } from '@zhin.js/host-http';
4
+ import { type MessageRef } from '@zhin.js/im-contract';
5
+ import type { CapabilityId } from 'zhin.js';
6
+ import { type CreateDiscordClient, type DiscordClientTransport } from './gateway.js';
7
+ import { type DiscordButtonInbound, type DiscordInboundMessage, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig } from './protocol.js';
8
+ export type { CreateDiscordClient, DiscordClientTransport, } from './gateway.js';
9
+ export interface DiscordEndpointOptions {
10
+ readonly id: CapabilityId;
11
+ readonly config: ResolvedDiscordGatewayConfig;
12
+ readonly createClient?: CreateDiscordClient;
13
+ readonly fetch?: typeof globalThis.fetch;
14
+ }
15
+ export declare class DiscordGatewayEndpoint extends Endpoint<DiscordClientTransport> {
16
+ #private;
17
+ readonly management: EndpointManagement;
18
+ readonly control: EndpointControl;
19
+ readonly content: EndpointContentPort;
20
+ constructor(options: DiscordEndpointOptions);
21
+ /** The actual discord.js-compatible client used by this connection. */
22
+ get client(): DiscordClientTransport;
23
+ start(): Promise<void>;
24
+ open(): void;
25
+ close(): void;
26
+ stop(): Promise<void>;
27
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
28
+ recallMessage(message: MessageRef): Promise<void>;
29
+ /** Test / internal: admit a message when open. */
30
+ admit(msg: DiscordInboundMessage): void;
31
+ /** Test / internal: admit a button interaction when open. */
32
+ admitButton(interaction: DiscordButtonInbound): void;
33
+ }
34
+ export interface DiscordInteractionsEndpointOptions {
35
+ readonly id: CapabilityId;
36
+ readonly http: HttpHost;
37
+ readonly config: ResolvedDiscordInteractionsConfig;
38
+ readonly fetch?: typeof globalThis.fetch;
39
+ }
40
+ /** Minimal Discord REST client used when Gateway is intentionally disabled. */
41
+ export declare class DiscordRestClient {
42
+ readonly token: string;
43
+ readonly fetch: typeof globalThis.fetch;
44
+ constructor(token: string, fetch?: typeof globalThis.fetch);
45
+ request<T = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', path: string, body?: unknown): Promise<T>;
46
+ createMessage(channelId: string, body: unknown): Promise<{
47
+ id?: string;
48
+ }>;
49
+ deleteMessage(channelId: string, messageId: string): Promise<void>;
50
+ }
51
+ export declare class DiscordInteractionsEndpoint extends Endpoint<DiscordRestClient> {
52
+ #private;
53
+ readonly client: DiscordRestClient;
54
+ readonly control: EndpointControl;
55
+ readonly content: EndpointContentPort;
56
+ constructor(options: DiscordInteractionsEndpointOptions);
57
+ get isOpen(): boolean;
58
+ get config(): ResolvedDiscordInteractionsConfig;
59
+ start(): Promise<void>;
60
+ open(): void;
61
+ close(): void;
62
+ stop(): Promise<void>;
63
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
64
+ recallMessage(message: MessageRef): Promise<void>;
65
+ admit(msg: DiscordInboundMessage): void;
66
+ admitPlatform(event: Record<string, unknown>): void;
67
+ }
68
+ /**
69
+ * DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
70
+ * 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
71
+ */
72
+ export declare function createDiscordEndpointManagement(requireClient: () => DiscordClientTransport): EndpointManagement;
@@ -0,0 +1,458 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ /**
3
+ * DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
4
+ */
5
+ import { ChannelType } from 'discord.js';
6
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
+ import { connectDiscordGatewayClient, defaultCreateClient, DEFAULT_INTENTS, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
8
+ import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
9
+ import { registerDiscordInteractionRoutes } from './webhook.js';
10
+ import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
11
+ const DISCORD_API = 'https://discord.com/api/v10';
12
+ /** 出站 HTTP 调用统一 30s 超时。 */
13
+ const OUTBOUND_TIMEOUT_MS = 30_000;
14
+ export class DiscordGatewayEndpoint extends Endpoint {
15
+ #logger;
16
+ #options;
17
+ #createClient;
18
+ #fetch;
19
+ #client = null;
20
+ #open = false;
21
+ #started = false;
22
+ management = createDiscordEndpointManagement(() => this.#requireClient());
23
+ control = Object.freeze({
24
+ recall: (message) => this.recallMessage(message),
25
+ addReaction: async (message, emoji, hint) => {
26
+ const channelId = hint?.channelId ?? message.conversation.id;
27
+ if (!channelId || !message.id)
28
+ return null;
29
+ await this.#addReaction(channelId, message.id, emoji);
30
+ return emoji;
31
+ },
32
+ });
33
+ content = Object.freeze({
34
+ resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
35
+ });
36
+ constructor(options) {
37
+ super();
38
+ this.#logger = getAdapterLogger('discord', options.config.id);
39
+ this.#options = options;
40
+ this.#createClient = options.createClient ?? defaultCreateClient;
41
+ this.#fetch = options.fetch ?? globalThis.fetch;
42
+ }
43
+ /** The actual discord.js-compatible client used by this connection. */
44
+ get client() {
45
+ return this.#requireClient();
46
+ }
47
+ async start() {
48
+ if (this.#started)
49
+ return;
50
+ this.#started = true;
51
+ try {
52
+ const intents = this.#options.config.intents?.length
53
+ ? [...this.#options.config.intents]
54
+ : DEFAULT_INTENTS;
55
+ this.#client = this.#createClient(intents);
56
+ await connectDiscordGatewayClient(this.#client, this.#options.config, {
57
+ onPlatformEvent: (name, event) => {
58
+ void this.#emitPlatformEvent(name, event);
59
+ },
60
+ onMessage: (msg) => this.admit(msg),
61
+ onButton: (interaction) => this.admitButton(interaction),
62
+ onGuildMemberAdd: (member) => {
63
+ receiveDiscordGuildMemberSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, 'member_increase', member, this.#logger);
64
+ },
65
+ onGuildMemberRemove: (member) => {
66
+ receiveDiscordGuildMemberSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, 'member_decrease', member, this.#logger);
67
+ },
68
+ });
69
+ this.#logger.info(formatCompact({
70
+ op: 'connect',
71
+ endpoint: this.#options.config.id,
72
+ mode: 'gateway',
73
+ user: this.#client.user?.tag,
74
+ }));
75
+ }
76
+ catch (error) {
77
+ await this.stop();
78
+ this.#logger.error('Failed to connect Discord gateway:', error);
79
+ throw error;
80
+ }
81
+ }
82
+ open() {
83
+ this.#open = true;
84
+ }
85
+ close() {
86
+ this.#open = false;
87
+ }
88
+ async stop() {
89
+ this.#open = false;
90
+ if (this.#client) {
91
+ try {
92
+ this.#client.removeAllListeners();
93
+ await this.#client.destroy();
94
+ }
95
+ catch {
96
+ /* ignore */
97
+ }
98
+ this.#client = null;
99
+ }
100
+ this.#started = false;
101
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
102
+ }
103
+ async send({ conversation, payload }) {
104
+ const body = formatOutboundBody(payload);
105
+ const snowflake = await this.#sendBody(conversation.id, body);
106
+ this.#logger.debug(formatCompact({
107
+ op: 'discord_send',
108
+ endpoint: this.#options.config.id,
109
+ target: conversation.id,
110
+ messageId: snowflake,
111
+ }));
112
+ return snowflake;
113
+ }
114
+ async recallMessage(message) {
115
+ if (!message.id)
116
+ return;
117
+ const channel = await this.#requireClient().channels.fetch(message.conversation.id);
118
+ if (!channel?.isTextBased() || !channel.messages)
119
+ return;
120
+ const msg = await channel.messages.fetch(message.id);
121
+ await msg.delete();
122
+ }
123
+ /** Test / internal: admit a message when open. */
124
+ admit(msg) {
125
+ if (!this.#open)
126
+ return;
127
+ if (msg.authorBot)
128
+ return;
129
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
130
+ void this.emit('message.receive', {
131
+ conversation,
132
+ message: { conversation, id: msg.id },
133
+ content: formatInboundContent(msg),
134
+ segments: formatInboundSegments(msg),
135
+ sender: {
136
+ id: msg.authorId,
137
+ name: senderDisplayName(msg) || undefined,
138
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
139
+ },
140
+ endpointId: this.#options.config.id,
141
+ ...(msg.mentionedBot ? { mentioned: true } : {}),
142
+ metadata: Object.freeze({
143
+ channelKind: msg.channelKind,
144
+ userId: msg.authorId,
145
+ guildId: msg.guildId,
146
+ permissions: msg.permissionTokens,
147
+ role: resolveSenderRole(msg),
148
+ }),
149
+ }).catch((err) => {
150
+ this.#logger.warn(formatCompact({
151
+ op: 'discord_gateway_receive_failed',
152
+ target: `${conversation.kind}:${conversation.id}`,
153
+ error: err instanceof Error ? err.message : String(err),
154
+ }));
155
+ });
156
+ }
157
+ /** Test / internal: admit a button interaction when open. */
158
+ admitButton(interaction) {
159
+ if (!this.#open)
160
+ return;
161
+ const conversation = discordInboundConversation(String(this.#options.id), interaction);
162
+ void this.emit('message.receive', {
163
+ conversation,
164
+ message: { conversation, id: interaction.id },
165
+ content: formatButtonContent(interaction),
166
+ segments: formatButtonSegments(interaction),
167
+ sender: { id: interaction.userId, name: interaction.userName },
168
+ endpointId: this.#options.config.id,
169
+ metadata: Object.freeze({
170
+ eventType: 'button',
171
+ payload: interaction.customId,
172
+ sourceMessageId: interaction.sourceMessageId,
173
+ }),
174
+ }).catch((err) => {
175
+ this.#logger.warn(formatCompact({
176
+ op: 'discord_gateway_receive_failed',
177
+ target: `${conversation.kind}:${conversation.id}`,
178
+ error: err instanceof Error ? err.message : String(err),
179
+ }));
180
+ });
181
+ }
182
+ async #addReaction(channelId, messageId, emoji) {
183
+ const channel = await this.#requireClient().channels.fetch(channelId);
184
+ if (!channel?.isTextBased() || !channel.messages) {
185
+ throw new Error(`Channel ${channelId} 不是文本频道`);
186
+ }
187
+ const message = await channel.messages.fetch(messageId);
188
+ await message.react(emoji);
189
+ }
190
+ async #emitPlatformEvent(name, event) {
191
+ await this.emitPlatform(name, event).catch((error) => {
192
+ this.#logger.warn(formatCompact({
193
+ op: 'discord_platform_event_failed',
194
+ event: name,
195
+ error: error instanceof Error ? error.message : String(error),
196
+ }));
197
+ });
198
+ }
199
+ async #sendBody(channelId, body) {
200
+ const channel = await this.#requireClient().channels.fetch(channelId);
201
+ if (!channel || !channel.isTextBased() || !channel.send) {
202
+ throw new Error(`Channel ${channelId} is not a text channel`);
203
+ }
204
+ const options = await toMessageCreateOptions(body);
205
+ const result = await channel.send(options);
206
+ return result.id;
207
+ }
208
+ #requireClient() {
209
+ if (!this.#client)
210
+ throw new Error('Discord client not connected');
211
+ return this.#client;
212
+ }
213
+ }
214
+ /** Minimal Discord REST client used when Gateway is intentionally disabled. */
215
+ export class DiscordRestClient {
216
+ token;
217
+ fetch;
218
+ constructor(token, fetch = globalThis.fetch) {
219
+ this.token = token;
220
+ this.fetch = fetch;
221
+ }
222
+ async request(method, path, body) {
223
+ const response = await this.fetch(`${DISCORD_API}${path}`, {
224
+ method,
225
+ headers: {
226
+ Authorization: `Bot ${this.token}`,
227
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
228
+ },
229
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
230
+ signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
231
+ });
232
+ const text = await response.text();
233
+ if (!response.ok) {
234
+ throw new Error(`Discord API ${method} ${path} failed (${response.status}): ${text.slice(0, 200)}`);
235
+ }
236
+ return (text ? JSON.parse(text) : undefined);
237
+ }
238
+ createMessage(channelId, body) {
239
+ return this.request('POST', `/channels/${channelId}/messages`, body);
240
+ }
241
+ deleteMessage(channelId, messageId) {
242
+ return this.request('DELETE', `/channels/${channelId}/messages/${messageId}`);
243
+ }
244
+ }
245
+ export class DiscordInteractionsEndpoint extends Endpoint {
246
+ client;
247
+ #logger;
248
+ #options;
249
+ #fetch;
250
+ #routeReleases = [];
251
+ #open = false;
252
+ #started = false;
253
+ control = Object.freeze({
254
+ recall: (message) => this.recallMessage(message),
255
+ });
256
+ content = Object.freeze({
257
+ resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
258
+ });
259
+ constructor(options) {
260
+ super();
261
+ this.#logger = getAdapterLogger('discord', options.config.id);
262
+ this.#options = options;
263
+ this.#fetch = options.fetch ?? globalThis.fetch;
264
+ this.client = new DiscordRestClient(options.config.token, this.#fetch);
265
+ }
266
+ get isOpen() {
267
+ return this.#open;
268
+ }
269
+ get config() {
270
+ return this.#options.config;
271
+ }
272
+ async start() {
273
+ if (this.#started)
274
+ return;
275
+ this.#started = true;
276
+ this.#routeReleases.push(...registerDiscordInteractionRoutes(this.#options.http, this));
277
+ this.#logger.info(formatCompact({
278
+ op: 'connect',
279
+ endpoint: this.#options.config.id,
280
+ mode: 'interactions',
281
+ path: this.#options.config.interactionsPath,
282
+ }));
283
+ }
284
+ open() {
285
+ this.#open = true;
286
+ }
287
+ close() {
288
+ this.#open = false;
289
+ }
290
+ async stop() {
291
+ this.#open = false;
292
+ for (const release of this.#routeReleases.splice(0))
293
+ release();
294
+ this.#started = false;
295
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
296
+ }
297
+ async send({ conversation, payload }) {
298
+ const body = formatOutboundBody(payload);
299
+ const data = await this.client.createMessage(conversation.id, body);
300
+ return data.id ?? '';
301
+ }
302
+ async recallMessage(message) {
303
+ if (!message.id)
304
+ return;
305
+ await this.client.deleteMessage(message.conversation.id, message.id);
306
+ }
307
+ admit(msg) {
308
+ if (!this.#open)
309
+ return;
310
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
311
+ void this.emit('message.receive', {
312
+ conversation,
313
+ message: { conversation, id: msg.id },
314
+ content: formatInboundContent(msg),
315
+ segments: formatInboundSegments(msg),
316
+ sender: {
317
+ id: msg.authorId,
318
+ name: senderDisplayName(msg) || undefined,
319
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
320
+ },
321
+ endpointId: this.#options.config.id,
322
+ metadata: Object.freeze({
323
+ channelKind: msg.channelKind,
324
+ userId: msg.authorId,
325
+ guildId: msg.guildId,
326
+ eventType: 'application_command',
327
+ }),
328
+ }).catch((err) => {
329
+ this.#logger.warn(formatCompact({
330
+ op: 'discord_gateway_receive_failed',
331
+ target: `${conversation.kind}:${conversation.id}`,
332
+ error: err instanceof Error ? err.message : String(err),
333
+ }));
334
+ });
335
+ }
336
+ admitPlatform(event) {
337
+ if (!this.#open)
338
+ return;
339
+ const type = typeof event.type === 'number' ? `interaction.${event.type}` : 'interaction';
340
+ void this.emitPlatform(type, event).catch((error) => {
341
+ this.#logger.warn(formatCompact({
342
+ op: 'discord_platform_event_failed',
343
+ event: type,
344
+ error: error instanceof Error ? error.message : String(error),
345
+ }));
346
+ });
347
+ }
348
+ }
349
+ async function resolveDiscordContent(fetch, token, reference, context) {
350
+ if (reference.kind === 'forward') {
351
+ return Object.freeze({ status: 'unsupported', code: 'discord_merged_forward_unavailable' });
352
+ }
353
+ if (reference.kind === 'media') {
354
+ return reference.media.kind === 'file'
355
+ ? Object.freeze({ status: 'unsupported', code: 'discord_opaque_media_unavailable' })
356
+ : Object.freeze({ status: 'resolved', reference, value: reference.media });
357
+ }
358
+ try {
359
+ context.signal.throwIfAborted();
360
+ const response = await fetch(`${DISCORD_API}/channels/${reference.message.conversation.id}/messages/${reference.message.id}`, { headers: { Authorization: `Bot ${token}` }, signal: context.signal });
361
+ if (response.status === 404)
362
+ return Object.freeze({ status: 'not_found', code: 'discord_message_not_found' });
363
+ if (response.status === 403)
364
+ return Object.freeze({ status: 'forbidden', code: 'discord_message_forbidden' });
365
+ if (!response.ok)
366
+ return Object.freeze({ status: 'failed', code: 'discord_message_fetch_failed' });
367
+ const row = await response.json();
368
+ const author = row.author;
369
+ const attachments = Array.isArray(row.attachments) ? row.attachments : [];
370
+ const segments = [];
371
+ if (typeof row.content === 'string' && row.content.trim())
372
+ segments.push({ type: 'text', data: { text: row.content } });
373
+ for (const item of attachments.slice(0, context.maxEntries)) {
374
+ const attachment = item;
375
+ if (typeof attachment.url !== 'string')
376
+ continue;
377
+ const mime = typeof attachment.content_type === 'string' ? attachment.content_type : undefined;
378
+ const type = mime?.startsWith('image/') ? 'image' : mime?.startsWith('audio/') ? 'audio' : mime?.startsWith('video/') ? 'video' : 'file';
379
+ segments.push({ type, data: { media: { kind: 'url', value: attachment.url, ...(mime ? { mime_type: mime } : {}), ...(attachment.filename ? { file_name: String(attachment.filename) } : {}), ...(typeof attachment.size === 'number' ? { size: attachment.size } : {}) } } });
380
+ }
381
+ return Object.freeze({
382
+ status: 'resolved',
383
+ reference,
384
+ value: Object.freeze({
385
+ ref: reference.message,
386
+ ...(author?.id ? { actor: Object.freeze({ id: String(author.id), ...(author.global_name || author.username ? { displayName: String(author.global_name ?? author.username) } : {}) }) } : {}),
387
+ segments: Object.freeze(segments),
388
+ timestamp: typeof row.timestamp === 'string' ? Date.parse(row.timestamp) : Date.now(),
389
+ }),
390
+ });
391
+ }
392
+ catch (error) {
393
+ if (context.signal.aborted)
394
+ return Object.freeze({ status: 'expired', code: 'turn_aborted' });
395
+ return Object.freeze({ status: 'failed', code: 'discord_content_resolution_failed', message: error instanceof Error ? error.message : String(error) });
396
+ }
397
+ }
398
+ /**
399
+ * Discord snowflake 是 64 位整数的字符串形式,超出 Number.MAX_SAFE_INTEGER,
400
+ * Number() 转换会丢精度。Console 社交面只把 group_id 当 JSON 值透传、
401
+ * 并以字符串回传给 listGroupMembers,因此这里保留原始字符串(仅按契约
402
+ * 类型声明强转),是全链路最不丢信息的方案。
403
+ */
404
+ function toGroupId(id) {
405
+ return id;
406
+ }
407
+ /**
408
+ * DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
409
+ * 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
410
+ */
411
+ export function createDiscordEndpointManagement(requireClient) {
412
+ return Object.freeze({
413
+ async listGroups() {
414
+ const groups = [];
415
+ for (const guild of requireClient().guilds.cache.values()) {
416
+ if (!guild?.id)
417
+ continue;
418
+ groups.push({
419
+ group_id: toGroupId(String(guild.id)),
420
+ name: String(guild.name ?? guild.id),
421
+ });
422
+ }
423
+ return groups;
424
+ },
425
+ async listChannels() {
426
+ const channels = [];
427
+ for (const guild of requireClient().guilds.cache.values()) {
428
+ if (!guild?.id)
429
+ continue;
430
+ const guildId = String(guild.id);
431
+ const guildName = String(guild.name ?? guildId);
432
+ for (const channel of guild.channels?.cache?.values() ?? []) {
433
+ if (!channel?.id)
434
+ continue;
435
+ if (channel.type !== ChannelType.GuildText)
436
+ continue;
437
+ channels.push({
438
+ id: String(channel.id),
439
+ name: channel.name ? String(channel.name) : undefined,
440
+ parent: { type: 'guild', id: guildId, name: guildName },
441
+ });
442
+ }
443
+ }
444
+ return channels;
445
+ },
446
+ async listGroupMembers(groupId) {
447
+ const guild = await requireClient().guilds.fetch(groupId);
448
+ const members = await guild.members.fetch({ limit: 100 });
449
+ return [...members.values()].map((member) => ({
450
+ id: member.id,
451
+ username: member.user.username,
452
+ nickname: member.nickname,
453
+ roles: member.roles.cache.map((role) => role.id),
454
+ joined_at: member.joinedAt?.toISOString(),
455
+ }));
456
+ },
457
+ });
458
+ }
@@ -0,0 +1,144 @@
1
+ import { GatewayIntentBits, type MessageCreateOptions } from 'discord.js';
2
+ import { type DiscordButtonInbound, type DiscordInboundMessage, type DiscordOutboundBody, type ResolvedDiscordGatewayConfig } from './protocol.js';
3
+ 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
+ login(token: string): Promise<string>;
7
+ destroy(): Promise<void>;
8
+ on(event: string, listener: (...args: unknown[]) => void): void;
9
+ once(event: string, listener: (...args: unknown[]) => void): void;
10
+ removeAllListeners(): void;
11
+ readonly user?: {
12
+ readonly id: string;
13
+ readonly tag?: string;
14
+ setActivity?(name: string, options?: {
15
+ type?: number;
16
+ url?: string;
17
+ }): void;
18
+ } | null;
19
+ channels: {
20
+ fetch(id: string): Promise<{
21
+ id: string;
22
+ type: number;
23
+ isTextBased(): boolean;
24
+ send?(options: MessageCreateOptions): Promise<{
25
+ id: string;
26
+ }>;
27
+ messages?: {
28
+ fetch(id: string): Promise<{
29
+ delete(): Promise<unknown>;
30
+ react(emoji: string): Promise<unknown>;
31
+ reactions: {
32
+ resolve(emoji: unknown): {
33
+ users: {
34
+ remove(userId: string): Promise<unknown>;
35
+ };
36
+ } | null;
37
+ cache: {
38
+ find(fn: (r: {
39
+ emoji: {
40
+ toString(): string;
41
+ name?: string | null;
42
+ id?: string | null;
43
+ };
44
+ }) => boolean): {
45
+ users: {
46
+ remove(userId: string): Promise<unknown>;
47
+ };
48
+ } | undefined;
49
+ };
50
+ };
51
+ }>;
52
+ };
53
+ threads?: {
54
+ create(options: Record<string, unknown>): Promise<{
55
+ id: string;
56
+ }>;
57
+ };
58
+ availableTags?: Array<{
59
+ id: string;
60
+ name: string;
61
+ }>;
62
+ } | null>;
63
+ };
64
+ guilds: {
65
+ fetch(id: string): Promise<{
66
+ id: string;
67
+ name: string;
68
+ ownerId: string;
69
+ memberCount: number;
70
+ createdAt?: Date | null;
71
+ iconURL?(options?: {
72
+ size?: number;
73
+ }): string | null;
74
+ roles: {
75
+ fetch(): Promise<unknown>;
76
+ cache: Map<string, {
77
+ id: string;
78
+ name: string;
79
+ hexColor: string;
80
+ position: number;
81
+ permissions: {
82
+ bitfield: bigint;
83
+ };
84
+ }> | {
85
+ map(fn: (role: {
86
+ id: string;
87
+ name: string;
88
+ hexColor: string;
89
+ position: number;
90
+ permissions: {
91
+ bitfield: bigint;
92
+ };
93
+ }) => unknown): unknown[];
94
+ };
95
+ };
96
+ members: {
97
+ fetch(userId: string | {
98
+ limit?: number;
99
+ }): Promise<unknown>;
100
+ ban(userId: string, options?: {
101
+ reason?: string;
102
+ deleteMessageSeconds?: number;
103
+ }): Promise<unknown>;
104
+ unban(userId: string, reason?: string): Promise<unknown>;
105
+ };
106
+ }>;
107
+ cache: {
108
+ values(): IterableIterator<{
109
+ id: string;
110
+ name: string;
111
+ channels: {
112
+ cache: {
113
+ values(): IterableIterator<{
114
+ id: string;
115
+ name?: string;
116
+ type: number;
117
+ }>;
118
+ };
119
+ };
120
+ }>;
121
+ };
122
+ };
123
+ }
124
+ export type CreateDiscordClient = (intents: readonly number[]) => DiscordClientTransport;
125
+ export declare function defaultCreateClient(intents: readonly number[]): DiscordClientTransport;
126
+ export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
127
+ export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
128
+ export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;
129
+ export interface DiscordGatewayConnectHandlers {
130
+ onPlatformEvent(name: string, event: unknown): void;
131
+ onMessage(msg: DiscordInboundMessage): void;
132
+ onButton(interaction: DiscordButtonInbound): void;
133
+ onGuildMemberAdd?(member: {
134
+ guildId: string;
135
+ userId: string;
136
+ userName?: string;
137
+ }): void;
138
+ onGuildMemberRemove?(member: {
139
+ guildId: string;
140
+ userId: string;
141
+ userName?: string;
142
+ }): void;
143
+ }
144
+ export declare function connectDiscordGatewayClient(client: DiscordClientTransport, config: ResolvedDiscordGatewayConfig, handlers: DiscordGatewayConnectHandlers): Promise<void>;