@zhin.js/adapter-discord 5.0.1 → 5.0.3
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/CHANGELOG.md +55 -0
- package/README.md +51 -76
- package/adapters/discord.ts +46 -0
- package/agent/tools/add_role.ts +24 -0
- package/agent/tools/create_thread.ts +25 -0
- package/agent/tools/forum_post.ts +24 -0
- package/agent/tools/list_roles.ts +22 -0
- package/agent/tools/react.ts +22 -0
- package/agent/tools/remove_role.ts +24 -0
- package/agent/tools/send_embed.ts +37 -0
- package/lib/discord-agent-deps.d.ts +35 -0
- package/lib/discord-agent-deps.js +32 -0
- package/lib/endpoint.d.ts +66 -119
- package/lib/endpoint.js +308 -1047
- package/lib/gateway.d.ts +122 -0
- package/lib/gateway.js +235 -0
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -330
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +135 -0
- package/lib/protocol.js +234 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +86 -0
- package/package.json +48 -31
- package/plugin.ts +13 -0
- package/schema.json +63 -0
- package/src/discord-agent-deps.ts +79 -0
- package/src/endpoint.ts +385 -1167
- package/src/gateway.ts +337 -0
- package/src/index.ts +55 -332
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +392 -0
- package/src/webhook.ts +121 -0
- package/client/Dashboard.tsx +0 -195
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -29
- package/lib/adapter.d.ts +0 -25
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -96
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint-interactions.d.ts +0 -34
- package/lib/endpoint-interactions.d.ts.map +0 -1
- package/lib/endpoint-interactions.js +0 -284
- package/lib/endpoint-interactions.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -49
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -108
- package/src/endpoint-interactions.ts +0 -354
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -60
- /package/{skills/discord → agent}/PERMITS.md +0 -0
- /package/{skills/discord/SKILL.md → agent/skills/discord.md} +0 -0
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
Client,
|
|
5
|
+
GatewayIntentBits,
|
|
6
|
+
EmbedBuilder,
|
|
7
|
+
AttachmentBuilder,
|
|
8
|
+
ActionRowBuilder,
|
|
9
|
+
ButtonBuilder,
|
|
10
|
+
ButtonStyle,
|
|
11
|
+
REST,
|
|
12
|
+
Routes,
|
|
13
|
+
PermissionFlagsBits,
|
|
14
|
+
type MessageCreateOptions,
|
|
15
|
+
type Message as DiscordMessage,
|
|
16
|
+
} from 'discord.js';
|
|
17
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
18
|
+
import {
|
|
19
|
+
activityTypeCode,
|
|
20
|
+
resolveChannelKind,
|
|
21
|
+
type DiscordButtonInbound,
|
|
22
|
+
type DiscordInboundMessage,
|
|
23
|
+
type DiscordOutboundBody,
|
|
24
|
+
type ResolvedDiscordGatewayConfig,
|
|
25
|
+
} from './protocol.js';
|
|
26
|
+
|
|
27
|
+
const logger = getLogger('discord');
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_INTENTS = [
|
|
30
|
+
GatewayIntentBits.Guilds,
|
|
31
|
+
GatewayIntentBits.GuildMessages,
|
|
32
|
+
GatewayIntentBits.MessageContent,
|
|
33
|
+
GatewayIntentBits.DirectMessages,
|
|
34
|
+
GatewayIntentBits.GuildMembers,
|
|
35
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Minimal client surface used by the endpoint (real discord.js or test mock). */
|
|
39
|
+
export interface DiscordClientTransport {
|
|
40
|
+
login(token: string): Promise<string>;
|
|
41
|
+
destroy(): Promise<void>;
|
|
42
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
43
|
+
once(event: string, listener: (...args: unknown[]) => void): void;
|
|
44
|
+
removeAllListeners(): void;
|
|
45
|
+
readonly user?: {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly tag?: string;
|
|
48
|
+
setActivity?(name: string, options?: { type?: number; url?: string }): void;
|
|
49
|
+
} | null;
|
|
50
|
+
channels: {
|
|
51
|
+
fetch(id: string): Promise<{
|
|
52
|
+
id: string;
|
|
53
|
+
type: number;
|
|
54
|
+
isTextBased(): boolean;
|
|
55
|
+
send?(options: MessageCreateOptions): Promise<{ id: string }>;
|
|
56
|
+
messages?: {
|
|
57
|
+
fetch(id: string): Promise<{
|
|
58
|
+
react(emoji: string): Promise<unknown>;
|
|
59
|
+
reactions: {
|
|
60
|
+
resolve(emoji: unknown): { users: { remove(userId: string): Promise<unknown> } } | null;
|
|
61
|
+
cache: { find(fn: (r: { emoji: { toString(): string; name?: string | null; id?: string | null } }) => boolean): { users: { remove(userId: string): Promise<unknown> } } | undefined };
|
|
62
|
+
};
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
threads?: {
|
|
66
|
+
create(options: Record<string, unknown>): Promise<{ id: string }>;
|
|
67
|
+
};
|
|
68
|
+
availableTags?: Array<{ id: string; name: string }>;
|
|
69
|
+
} | null>;
|
|
70
|
+
};
|
|
71
|
+
guilds: {
|
|
72
|
+
fetch(id: string): Promise<{
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
ownerId: string;
|
|
76
|
+
memberCount: number;
|
|
77
|
+
createdAt?: Date | null;
|
|
78
|
+
iconURL?(options?: { size?: number }): string | null;
|
|
79
|
+
roles: {
|
|
80
|
+
fetch(): Promise<unknown>;
|
|
81
|
+
cache: Map<string, {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
hexColor: string;
|
|
85
|
+
position: number;
|
|
86
|
+
permissions: { bitfield: bigint };
|
|
87
|
+
}> | { map(fn: (role: {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
hexColor: string;
|
|
91
|
+
position: number;
|
|
92
|
+
permissions: { bitfield: bigint };
|
|
93
|
+
}) => unknown): unknown[] };
|
|
94
|
+
};
|
|
95
|
+
members: {
|
|
96
|
+
fetch(userId: string | { limit?: number }): Promise<unknown>;
|
|
97
|
+
ban(userId: string, options?: { reason?: string; deleteMessageSeconds?: number }): Promise<unknown>;
|
|
98
|
+
unban(userId: string, reason?: string): Promise<unknown>;
|
|
99
|
+
};
|
|
100
|
+
}>;
|
|
101
|
+
cache: { values(): IterableIterator<{ id: string }> };
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type CreateDiscordClient = (intents: readonly number[]) => DiscordClientTransport;
|
|
106
|
+
|
|
107
|
+
export function defaultCreateClient(intents: readonly number[]): DiscordClientTransport {
|
|
108
|
+
return new Client({ intents: [...intents] }) as unknown as DiscordClientTransport;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveSenderRole(msg: DiscordInboundMessage): string | undefined {
|
|
112
|
+
if (msg.isGuildOwner) return 'owner';
|
|
113
|
+
const tokens = msg.permissionTokens ?? [];
|
|
114
|
+
if (tokens.includes('ADMINISTRATOR') || tokens.includes('MODERATE_MEMBERS')) return 'admin';
|
|
115
|
+
if (msg.guildId) return 'member';
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null {
|
|
120
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
121
|
+
const msg = raw as DiscordMessage;
|
|
122
|
+
if (!msg.author || !msg.channel) return null;
|
|
123
|
+
|
|
124
|
+
const permissionTokens: string[] = [];
|
|
125
|
+
let isGuildOwner = false;
|
|
126
|
+
const member = msg.member;
|
|
127
|
+
const guild = msg.guild;
|
|
128
|
+
if (member && guild) {
|
|
129
|
+
const checks: Array<[bigint, string]> = [
|
|
130
|
+
[PermissionFlagsBits.Administrator, 'ADMINISTRATOR'],
|
|
131
|
+
[PermissionFlagsBits.ManageRoles, 'MANAGE_ROLES'],
|
|
132
|
+
[PermissionFlagsBits.ModerateMembers, 'MODERATE_MEMBERS'],
|
|
133
|
+
[PermissionFlagsBits.ManageChannels, 'MANAGE_CHANNELS'],
|
|
134
|
+
[PermissionFlagsBits.ManageGuild, 'MANAGE_GUILD'],
|
|
135
|
+
];
|
|
136
|
+
for (const [bit, name] of checks) {
|
|
137
|
+
if (member.permissions.has(bit)) permissionTokens.push(name);
|
|
138
|
+
}
|
|
139
|
+
if (guild.ownerId === msg.author.id) {
|
|
140
|
+
isGuildOwner = true;
|
|
141
|
+
permissionTokens.push('guild_owner', 'ADMINISTRATOR');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
id: msg.id,
|
|
147
|
+
content: msg.content ?? '',
|
|
148
|
+
channelId: msg.channel.id,
|
|
149
|
+
channelKind: resolveChannelKind(msg.channel.type),
|
|
150
|
+
authorId: msg.author.id,
|
|
151
|
+
authorName: member?.displayName || msg.author.displayName || msg.author.username,
|
|
152
|
+
authorBot: msg.author.bot,
|
|
153
|
+
createdTimestamp: msg.createdTimestamp,
|
|
154
|
+
guildId: guild?.id,
|
|
155
|
+
isGuildOwner,
|
|
156
|
+
permissionTokens,
|
|
157
|
+
attachments: [...msg.attachments.values()].map((a) => ({
|
|
158
|
+
id: a.id,
|
|
159
|
+
name: a.name ?? undefined,
|
|
160
|
+
url: a.url,
|
|
161
|
+
contentType: a.contentType ?? undefined,
|
|
162
|
+
size: a.size,
|
|
163
|
+
})),
|
|
164
|
+
embedTitles: msg.embeds.map((e) => e.title || e.description || 'embed').filter(Boolean) as string[],
|
|
165
|
+
stickerNames: [...msg.stickers.values()].map((s) => s.name),
|
|
166
|
+
replyToId: msg.reference?.messageId ?? undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions> {
|
|
171
|
+
const options: MessageCreateOptions = {};
|
|
172
|
+
if (body.content) options.content = body.content;
|
|
173
|
+
if (body.embeds?.length) {
|
|
174
|
+
options.embeds = body.embeds.map((data) => {
|
|
175
|
+
const embed = new EmbedBuilder();
|
|
176
|
+
if (data.title) embed.setTitle(String(data.title));
|
|
177
|
+
if (data.description) embed.setDescription(String(data.description));
|
|
178
|
+
if (data.color != null) embed.setColor(data.color as number);
|
|
179
|
+
if (data.url) embed.setURL(String(data.url));
|
|
180
|
+
const thumb = data.thumbnail as { url?: string } | undefined;
|
|
181
|
+
if (thumb?.url) embed.setThumbnail(thumb.url);
|
|
182
|
+
const image = data.image as { url?: string } | undefined;
|
|
183
|
+
if (image?.url) embed.setImage(image.url);
|
|
184
|
+
if (data.author) embed.setAuthor(data.author as { name: string });
|
|
185
|
+
if (data.footer) embed.setFooter(data.footer as { text: string });
|
|
186
|
+
if (data.timestamp) embed.setTimestamp(new Date(String(data.timestamp)));
|
|
187
|
+
if (Array.isArray(data.fields)) embed.addFields(data.fields as Array<{ name: string; value: string }>);
|
|
188
|
+
return embed;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (body.files?.length) {
|
|
192
|
+
const files: AttachmentBuilder[] = [];
|
|
193
|
+
for (const file of body.files) {
|
|
194
|
+
if (file.file && await fileExists(file.file)) {
|
|
195
|
+
files.push(new AttachmentBuilder(createReadStream(file.file), {
|
|
196
|
+
name: file.name || path.basename(file.file),
|
|
197
|
+
}));
|
|
198
|
+
} else if (file.url) {
|
|
199
|
+
files.push(new AttachmentBuilder(file.url, { name: file.name || 'attachment' }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (files.length) options.files = files;
|
|
203
|
+
}
|
|
204
|
+
if (body.components?.length) {
|
|
205
|
+
options.components = body.components.map((row) =>
|
|
206
|
+
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
207
|
+
...row.components.map((btn) => {
|
|
208
|
+
const b = new ButtonBuilder()
|
|
209
|
+
.setCustomId(btn.custom_id)
|
|
210
|
+
.setLabel(btn.label)
|
|
211
|
+
.setDisabled(!!btn.disabled);
|
|
212
|
+
if (btn.style === 4) b.setStyle(ButtonStyle.Danger);
|
|
213
|
+
else if (btn.style === 1) b.setStyle(ButtonStyle.Primary);
|
|
214
|
+
else b.setStyle(ButtonStyle.Secondary);
|
|
215
|
+
return b;
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return options;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function registerSlashCommands(
|
|
224
|
+
config: ResolvedDiscordGatewayConfig,
|
|
225
|
+
applicationId: string,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
if (!config.slashCommands?.length) return;
|
|
228
|
+
const rest = new REST({ version: '10' }).setToken(config.token);
|
|
229
|
+
if (config.globalCommands) {
|
|
230
|
+
await rest.put(Routes.applicationCommands(applicationId), {
|
|
231
|
+
body: config.slashCommands,
|
|
232
|
+
});
|
|
233
|
+
logger.info(formatCompact({ op: 'slash_commands', scope: 'global' }));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
238
|
+
try {
|
|
239
|
+
await fs.access(filePath);
|
|
240
|
+
return true;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface DiscordGatewayConnectHandlers {
|
|
247
|
+
onMessage(msg: DiscordInboundMessage): void;
|
|
248
|
+
onButton(interaction: DiscordButtonInbound): void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function connectDiscordGatewayClient(
|
|
252
|
+
client: DiscordClientTransport,
|
|
253
|
+
config: ResolvedDiscordGatewayConfig,
|
|
254
|
+
handlers: DiscordGatewayConnectHandlers,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
let settled = false;
|
|
258
|
+
|
|
259
|
+
client.on('messageCreate', (raw) => {
|
|
260
|
+
const msg = normalizeDiscordMessage(raw);
|
|
261
|
+
if (!msg) return;
|
|
262
|
+
// clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
|
|
263
|
+
const botId = client.user?.id;
|
|
264
|
+
const mentions = (raw as DiscordMessage).mentions;
|
|
265
|
+
const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
|
|
266
|
+
handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
client.on('interactionCreate', (raw) => {
|
|
270
|
+
const interaction = raw as {
|
|
271
|
+
isButton?(): boolean;
|
|
272
|
+
deferUpdate?(): Promise<unknown>;
|
|
273
|
+
id: string;
|
|
274
|
+
customId: string;
|
|
275
|
+
channel?: { id: string; type: number } | null;
|
|
276
|
+
user: { id: string; username?: string; displayName?: string };
|
|
277
|
+
message?: { id: string };
|
|
278
|
+
};
|
|
279
|
+
if (!interaction.isButton?.()) return;
|
|
280
|
+
void interaction.deferUpdate?.().catch(() => { /* already ack */ });
|
|
281
|
+
if (!interaction.channel) return;
|
|
282
|
+
handlers.onButton({
|
|
283
|
+
id: interaction.id,
|
|
284
|
+
customId: interaction.customId,
|
|
285
|
+
channelId: interaction.channel.id,
|
|
286
|
+
channelKind: resolveChannelKind(interaction.channel.type),
|
|
287
|
+
userId: interaction.user.id,
|
|
288
|
+
userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
|
|
289
|
+
sourceMessageId: interaction.message?.id,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
client.once('clientReady', () => {
|
|
294
|
+
void (async () => {
|
|
295
|
+
try {
|
|
296
|
+
if (config.defaultActivity && client.user?.setActivity) {
|
|
297
|
+
client.user.setActivity(config.defaultActivity.name, {
|
|
298
|
+
type: activityTypeCode(config.defaultActivity.type),
|
|
299
|
+
url: config.defaultActivity.url,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (config.enableSlashCommands && config.slashCommands?.length && client.user) {
|
|
303
|
+
await registerSlashCommands(config, client.user.id);
|
|
304
|
+
}
|
|
305
|
+
if (!settled) {
|
|
306
|
+
settled = true;
|
|
307
|
+
resolve();
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (!settled) {
|
|
311
|
+
settled = true;
|
|
312
|
+
reject(error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
client.on('error', (error) => {
|
|
319
|
+
logger.error('Discord client error:', error);
|
|
320
|
+
if (!settled) {
|
|
321
|
+
settled = true;
|
|
322
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
client.on('warn', (info) => {
|
|
327
|
+
logger.warn('Discord client warning:', info);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
client.login(config.token).catch((error) => {
|
|
331
|
+
if (!settled) {
|
|
332
|
+
settled = true;
|
|
333
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|