@zhin.js/adapter-discord 1.0.87 → 1.1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +949 -66
  2. package/PERMITS.md +25 -0
  3. package/README.md +60 -67
  4. package/adapters/discord/index.js +41 -0
  5. package/adapters/discord/index.ts +57 -0
  6. package/agents/discord/agent.json +20 -0
  7. package/agents/discord/boundaries.md +3 -0
  8. package/agents/discord/conventions.md +3 -0
  9. package/agents/discord/skills/discord/SKILL.md +21 -0
  10. package/agents/discord/skills/discord/tools/add_role/index.js +24 -0
  11. package/agents/discord/skills/discord/tools/add_role/index.ts +26 -0
  12. package/agents/discord/skills/discord/tools/create_thread/index.js +29 -0
  13. package/agents/discord/skills/discord/tools/create_thread/index.ts +28 -0
  14. package/agents/discord/skills/discord/tools/forum_post/index.js +33 -0
  15. package/agents/discord/skills/discord/tools/forum_post/index.ts +33 -0
  16. package/agents/discord/skills/discord/tools/list_roles/index.js +28 -0
  17. package/agents/discord/skills/discord/tools/list_roles/index.ts +34 -0
  18. package/agents/discord/skills/discord/tools/react/index.js +24 -0
  19. package/agents/discord/skills/discord/tools/react/index.ts +24 -0
  20. package/agents/discord/skills/discord/tools/remove_role/index.js +24 -0
  21. package/agents/discord/skills/discord/tools/remove_role/index.ts +26 -0
  22. package/agents/discord/skills/discord/tools/send_embed/index.js +43 -0
  23. package/agents/discord/skills/discord/tools/send_embed/index.ts +38 -0
  24. package/agents/discord/system.md +3 -0
  25. package/commands/discord/endpoint/add/[id]/index.js +3 -0
  26. package/commands/discord/endpoint/add/[id]/index.ts +3 -0
  27. package/commands/discord/endpoint/definition.js +19 -0
  28. package/commands/discord/endpoint/definition.ts +19 -0
  29. package/commands/discord/endpoint/list/index.js +3 -0
  30. package/commands/discord/endpoint/list/index.ts +3 -0
  31. package/commands/discord/endpoint/remove/[id]/index.js +3 -0
  32. package/commands/discord/endpoint/remove/[id]/index.ts +3 -0
  33. package/lib/client.d.ts +16 -0
  34. package/lib/client.js +8 -0
  35. package/lib/discord-runtime-state.d.ts +1 -0
  36. package/lib/discord-runtime-state.js +6 -0
  37. package/lib/endpoint.d.ts +72 -0
  38. package/lib/endpoint.js +458 -0
  39. package/lib/gateway.d.ts +144 -0
  40. package/lib/gateway.js +303 -0
  41. package/lib/index.d.ts +6 -18
  42. package/lib/index.js +6 -323
  43. package/lib/platform-permit.d.ts +14 -0
  44. package/lib/platform-permit.js +42 -0
  45. package/lib/protocol.d.ts +145 -0
  46. package/lib/protocol.js +320 -0
  47. package/lib/side-event-dispatch.d.ts +8 -0
  48. package/lib/side-event-dispatch.js +24 -0
  49. package/lib/webhook.d.ts +14 -0
  50. package/lib/webhook.js +88 -0
  51. package/package.json +74 -30
  52. package/plugin.js +19 -0
  53. package/schema.json +205 -0
  54. package/src/client.ts +24 -0
  55. package/src/discord-runtime-state.ts +7 -0
  56. package/src/endpoint.ts +562 -0
  57. package/src/gateway.ts +426 -0
  58. package/src/index.ts +57 -323
  59. package/src/platform-permit.ts +57 -0
  60. package/src/protocol.ts +475 -0
  61. package/src/side-event-dispatch.ts +37 -0
  62. package/src/webhook.ts +123 -0
  63. package/client/Dashboard.tsx +0 -195
  64. package/client/index.tsx +0 -11
  65. package/client/tsconfig.json +0 -7
  66. package/client/utils/api.ts +0 -17
  67. package/dist/index.js +0 -29
  68. package/lib/adapter.d.ts +0 -19
  69. package/lib/adapter.d.ts.map +0 -1
  70. package/lib/adapter.js +0 -90
  71. package/lib/adapter.js.map +0 -1
  72. package/lib/bot-interactions.d.ts +0 -33
  73. package/lib/bot-interactions.d.ts.map +0 -1
  74. package/lib/bot-interactions.js +0 -278
  75. package/lib/bot-interactions.js.map +0 -1
  76. package/lib/bot.d.ts +0 -117
  77. package/lib/bot.d.ts.map +0 -1
  78. package/lib/bot.js +0 -904
  79. package/lib/bot.js.map +0 -1
  80. package/lib/index.d.ts.map +0 -1
  81. package/lib/index.js.map +0 -1
  82. package/lib/types.d.ts +0 -49
  83. package/lib/types.d.ts.map +0 -1
  84. package/lib/types.js +0 -2
  85. package/lib/types.js.map +0 -1
  86. package/plugin.yml +0 -3
  87. package/skills/discord/SKILL.md +0 -104
  88. package/src/adapter.ts +0 -107
  89. package/src/bot-interactions.ts +0 -348
  90. package/src/bot.ts +0 -1037
  91. package/src/types.ts +0 -60
package/src/bot.ts DELETED
@@ -1,1037 +0,0 @@
1
- /**
2
- * Discord Bot 实现 (Gateway)
3
- */
4
- import {
5
- Client,
6
- GatewayIntentBits,
7
- Message as DiscordMessage,
8
- TextChannel,
9
- DMChannel,
10
- NewsChannel,
11
- ThreadChannel,
12
- EmbedBuilder,
13
- AttachmentBuilder,
14
- MessageCreateOptions,
15
- ChannelType,
16
- REST,
17
- Routes,
18
- ApplicationCommandData,
19
- ChatInputCommandInteraction,
20
- InteractionType,
21
- InteractionResponseType,
22
- GuildMember,
23
- PermissionsBitField,
24
- } from "discord.js";
25
- import {
26
- Bot,
27
- Message,
28
- SendOptions,
29
- SendContent,
30
- MessageSegment,
31
- segment,
32
- } from "zhin.js";
33
- import type { DiscordGatewayConfig, DiscordChannelMessage } from "./types.js";
34
- import type { DiscordAdapter } from "./adapter.js";
35
- import { createReadStream } from "fs";
36
- import { promises as fs } from "fs";
37
- import path from "path";
38
-
39
- export class DiscordBot
40
- extends Client
41
- implements Bot<DiscordGatewayConfig, DiscordChannelMessage> {
42
- private slashCommandHandlers: Map<
43
- string,
44
- (interaction: ChatInputCommandInteraction) => Promise<void>
45
- > = new Map();
46
- $connected: boolean = false;
47
- get pluginLogger() {
48
- return this.adapter.plugin.logger;
49
- }
50
-
51
- get $id() {
52
- return this.$config.name;
53
- }
54
-
55
- constructor(public adapter: DiscordAdapter, public $config: DiscordGatewayConfig) {
56
- const intents = $config.intents || [
57
- GatewayIntentBits.Guilds,
58
- GatewayIntentBits.GuildMessages,
59
- GatewayIntentBits.MessageContent,
60
- GatewayIntentBits.DirectMessages,
61
- GatewayIntentBits.GuildMembers,
62
- GatewayIntentBits.GuildMessageReactions,
63
- ];
64
-
65
- super({ intents });
66
- this.$connected = false;
67
- }
68
-
69
- private async handleDiscordMessage(
70
- msg: DiscordChannelMessage
71
- ): Promise<void> {
72
- // 忽略机器人消息
73
- if (msg.author.bot) return;
74
-
75
- const message = this.$formatMessage(msg);
76
- this.adapter.emit("message.receive", message);
77
- this.pluginLogger.debug(
78
- `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(
79
- message.$content
80
- )}`
81
- );
82
- }
83
-
84
- private async handleSlashCommand(
85
- interaction: ChatInputCommandInteraction
86
- ): Promise<void> {
87
- const commandName = interaction.commandName;
88
- const handler = this.slashCommandHandlers.get(commandName);
89
-
90
- if (handler) {
91
- try {
92
- await handler(interaction);
93
- this.pluginLogger.info(
94
- `Executed slash command: /${commandName} by ${interaction.user.tag}`
95
- );
96
- } catch (error) {
97
- this.pluginLogger.error(
98
- `Error executing slash command /${commandName}:`,
99
- error
100
- );
101
-
102
- const errorMessage = "An error occurred while executing this command.";
103
- if (interaction.replied || interaction.deferred) {
104
- await interaction.followUp({
105
- content: errorMessage,
106
- ephemeral: true,
107
- });
108
- } else {
109
- await interaction.reply({ content: errorMessage, ephemeral: true });
110
- }
111
- }
112
- } else {
113
- this.pluginLogger.warn(`Unknown slash command: /${commandName}`);
114
- if (!interaction.replied) {
115
- await interaction.reply({
116
- content: "Unknown command.",
117
- ephemeral: true,
118
- });
119
- }
120
- }
121
- }
122
-
123
- async $connect(): Promise<void> {
124
- return new Promise((resolve, reject) => {
125
- // 监听消息事件
126
- this.on("messageCreate", this.handleDiscordMessage.bind(this));
127
-
128
- // 监听交互事件(Slash Commands)
129
- if (this.$config.enableSlashCommands) {
130
- this.on("interactionCreate", async (interaction) => {
131
- if (interaction.isChatInputCommand()) {
132
- await this.handleSlashCommand(interaction);
133
- }
134
- });
135
- }
136
-
137
- // 监听就绪事件
138
- this.once("clientReady", async () => {
139
- this.$connected = true;
140
- this.pluginLogger.info(
141
- `Discord bot ${this.$config.name} connected successfully as ${this.user?.tag}`
142
- );
143
-
144
- // 设置活动状态
145
- if (this.$config.defaultActivity) {
146
- this.user?.setActivity(this.$config.defaultActivity.name, {
147
- type: this.getActivityType(this.$config.defaultActivity.type),
148
- url: this.$config.defaultActivity.url,
149
- });
150
- }
151
-
152
- // 注册 Slash Commands
153
- if (this.$config.enableSlashCommands && this.$config.slashCommands) {
154
- await this.registerSlashCommands();
155
- }
156
-
157
- resolve();
158
- });
159
-
160
- // 监听错误事件
161
- this.on("error", (error) => {
162
- this.pluginLogger.error("Discord client error:", error);
163
- this.$connected = false;
164
- reject(error);
165
- });
166
-
167
- // 登录
168
- this.login(this.$config.token).catch((error) => {
169
- this.pluginLogger.error("Failed to login to Discord:", error);
170
- this.$connected = false;
171
- reject(error);
172
- });
173
- });
174
- }
175
-
176
- async $disconnect(): Promise<void> {
177
- try {
178
- await this.destroy();
179
- this.$connected = false;
180
- this.pluginLogger.info(`Discord bot ${this.$config.name} disconnected`);
181
- } catch (error) {
182
- this.pluginLogger.error("Error disconnecting Discord bot:", error);
183
- throw error;
184
- }
185
- }
186
-
187
- $formatMessage(msg: DiscordChannelMessage): Message<DiscordChannelMessage> {
188
- // 确定聊天类型和ID
189
- let channelType: "private" | "group" | "channel";
190
- let channelId: string;
191
-
192
- if (msg.channel.type === ChannelType.DM) {
193
- channelType = "private";
194
- channelId = msg.channel.id;
195
- } else if (msg.channel.type === ChannelType.GroupDM) {
196
- channelType = "group";
197
- channelId = msg.channel.id;
198
- } else {
199
- channelType = "channel";
200
- channelId = msg.channel.id;
201
- }
202
-
203
- // 转换消息内容为 segment 格式
204
- const content = this.parseMessageContent(msg);
205
-
206
- const result = Message.from(msg, {
207
- $id: msg.id,
208
- $adapter: "discord",
209
- $bot: this.$config.name,
210
- $sender: {
211
- id: msg.author.id,
212
- name: msg.member?.displayName || msg.author.displayName,
213
- },
214
- $channel: {
215
- id: channelId,
216
- type: channelType,
217
- },
218
- $content: content,
219
- $raw: msg.content,
220
- $timestamp: msg.createdTimestamp,
221
- $recall: async () => {
222
- await msg.delete();
223
- },
224
- $reply: async (
225
- content: SendContent,
226
- quote?: boolean | string
227
- ): Promise<string> => {
228
- if (!Array.isArray(content)) content = [content];
229
-
230
- const sendOptions: MessageCreateOptions = {};
231
-
232
- // 处理回复消息
233
- if (quote) {
234
- const replyId = typeof quote === "boolean" ? result.$id : quote;
235
- try {
236
- const replyMessage = await msg.channel.messages.fetch(replyId);
237
- sendOptions.reply = { messageReference: replyMessage };
238
- } catch (error) {
239
- this.pluginLogger.warn(
240
- `Could not find message to reply to: ${replyId}`
241
- );
242
- }
243
- }
244
-
245
- const res = await this.adapter.sendMessage({
246
- context: "discord",
247
- bot: this.$config.name,
248
- id: msg.channel.id,
249
- type: msg.channel.type as any,
250
- content: content,
251
- });
252
- return res;
253
- },
254
- });
255
-
256
- return result;
257
- }
258
-
259
- // 解析 Discord 消息内容为 segment 格式
260
- parseMessageContent(msg: DiscordChannelMessage): MessageSegment[] {
261
- const segments: MessageSegment[] = [];
262
-
263
- // 回复消息处理
264
- if (msg.reference) {
265
- segments.push({
266
- type: "reply",
267
- data: {
268
- id: msg.reference.messageId,
269
- channel_id: msg.reference.channelId,
270
- guild_id: msg.reference.guildId,
271
- },
272
- });
273
- }
274
-
275
- // 文本消息(包含提及、表情等)
276
- if (msg.content) {
277
- segments.push(...this.parseTextContent(msg.content, msg));
278
- }
279
-
280
- // 附件消息
281
- for (const attachment of msg.attachments.values()) {
282
- segments.push(...this.parseAttachment(attachment));
283
- }
284
-
285
- // Embed 消息
286
- for (const embed of msg.embeds) {
287
- segments.push({
288
- type: "embed",
289
- data: {
290
- title: embed.title,
291
- description: embed.description,
292
- color: embed.color,
293
- url: embed.url,
294
- thumbnail: embed.thumbnail,
295
- image: embed.image,
296
- author: embed.author,
297
- footer: embed.footer,
298
- fields: embed.fields,
299
- timestamp: embed.timestamp,
300
- },
301
- });
302
- }
303
-
304
- // 贴纸消息
305
- for (const sticker of msg.stickers.values()) {
306
- segments.push({
307
- type: "sticker",
308
- data: {
309
- id: sticker.id,
310
- name: sticker.name,
311
- url: sticker.url,
312
- format: sticker.format,
313
- tags: sticker.tags,
314
- },
315
- });
316
- }
317
-
318
- return segments.length > 0
319
- ? segments
320
- : [{ type: "text", data: { text: "" } }];
321
- }
322
-
323
- // 解析文本内容,处理提及、频道引用、角色引用等
324
- parseTextContent(
325
- content: string,
326
- msg: DiscordChannelMessage
327
- ): MessageSegment[] {
328
- const segments: MessageSegment[] = [];
329
- let lastIndex = 0;
330
-
331
- // 匹配用户提及 <@!?用户ID>
332
- const userMentionRegex = /<@!?(\d+)>/g;
333
- // 匹配频道提及 <#频道ID>
334
- const channelMentionRegex = /<#(\d+)>/g;
335
- // 匹配角色提及 <@&角色ID>
336
- const roleMentionRegex = /<@&(\d+)>/g;
337
- // 匹配自定义表情 <:名称:ID> 或 <a:名称:ID>
338
- const emojiRegex = /<a?:(\w+):(\d+)>/g;
339
-
340
- const allMatches: Array<{
341
- match: RegExpExecArray;
342
- type: "user" | "channel" | "role" | "emoji";
343
- }> = [];
344
-
345
- // 收集所有匹配项
346
- let match;
347
- while ((match = userMentionRegex.exec(content)) !== null) {
348
- allMatches.push({ match, type: "user" });
349
- }
350
- while ((match = channelMentionRegex.exec(content)) !== null) {
351
- allMatches.push({ match, type: "channel" });
352
- }
353
- while ((match = roleMentionRegex.exec(content)) !== null) {
354
- allMatches.push({ match, type: "role" });
355
- }
356
- while ((match = emojiRegex.exec(content)) !== null) {
357
- allMatches.push({ match, type: "emoji" });
358
- }
359
-
360
- // 按位置排序
361
- allMatches.sort((a, b) => a.match.index! - b.match.index!);
362
-
363
- // 处理每个匹配项
364
- for (const { match, type } of allMatches) {
365
- const matchStart = match.index!;
366
- const matchEnd = matchStart + match[0].length;
367
-
368
- // 添加匹配项前的文本
369
- if (matchStart > lastIndex) {
370
- const beforeText = content.slice(lastIndex, matchStart);
371
- if (beforeText.trim()) {
372
- segments.push({ type: "text", data: { text: beforeText } });
373
- }
374
- }
375
-
376
- // 添加特殊内容段
377
- switch (type) {
378
- case "user":
379
- const userId = match[1];
380
- const user = msg.mentions.users.get(userId);
381
- segments.push({
382
- type: "at",
383
- data: {
384
- id: userId,
385
- name: user?.username || "Unknown",
386
- text: match[0],
387
- },
388
- });
389
- break;
390
-
391
- case "channel":
392
- const channelId = match[1];
393
- const channel = msg.mentions.channels.get(channelId);
394
- segments.push({
395
- type: "channel_mention",
396
- data: {
397
- id: channelId,
398
- name: (channel as any)?.name || "unknown-channel",
399
- text: match[0],
400
- },
401
- });
402
- break;
403
-
404
- case "role":
405
- const roleId = match[1];
406
- const role = msg.mentions.roles.get(roleId);
407
- segments.push({
408
- type: "role_mention",
409
- data: {
410
- id: roleId,
411
- name: role?.name || "unknown-role",
412
- text: match[0],
413
- },
414
- });
415
- break;
416
-
417
- case "emoji":
418
- const emojiName = match[1];
419
- const emojiId = match[2];
420
- const isAnimated = match[0].startsWith("<a:");
421
- segments.push({
422
- type: "emoji",
423
- data: {
424
- id: emojiId,
425
- name: emojiName,
426
- animated: isAnimated,
427
- url: `https://cdn.discordapp.com/emojis/${emojiId}.${isAnimated ? "gif" : "png"
428
- }`,
429
- text: match[0],
430
- },
431
- });
432
- break;
433
- }
434
-
435
- lastIndex = matchEnd;
436
- }
437
-
438
- // 添加最后剩余的文本
439
- if (lastIndex < content.length) {
440
- const remainingText = content.slice(lastIndex);
441
- if (remainingText.trim()) {
442
- segments.push({ type: "text", data: { text: remainingText } });
443
- }
444
- }
445
-
446
- return segments.length > 0
447
- ? segments
448
- : [{ type: "text", data: { text: content } }];
449
- }
450
-
451
- // 解析附件
452
- parseAttachment(attachment: any): MessageSegment[] {
453
- const segments: MessageSegment[] = [];
454
-
455
- if (attachment.contentType?.startsWith("image/")) {
456
- segments.push({
457
- type: "image",
458
- data: {
459
- id: attachment.id,
460
- name: attachment.name,
461
- url: attachment.url,
462
- proxy_url: attachment.proxyURL,
463
- size: attachment.size,
464
- width: attachment.width,
465
- height: attachment.height,
466
- content_type: attachment.contentType,
467
- },
468
- });
469
- } else if (attachment.contentType?.startsWith("audio/")) {
470
- segments.push({
471
- type: "audio",
472
- data: {
473
- id: attachment.id,
474
- name: attachment.name,
475
- url: attachment.url,
476
- proxy_url: attachment.proxyURL,
477
- size: attachment.size,
478
- content_type: attachment.contentType,
479
- },
480
- });
481
- } else if (attachment.contentType?.startsWith("video/")) {
482
- segments.push({
483
- type: "video",
484
- data: {
485
- id: attachment.id,
486
- name: attachment.name,
487
- url: attachment.url,
488
- proxy_url: attachment.proxyURL,
489
- size: attachment.size,
490
- width: attachment.width,
491
- height: attachment.height,
492
- content_type: attachment.contentType,
493
- },
494
- });
495
- } else {
496
- segments.push({
497
- type: "file",
498
- data: {
499
- id: attachment.id,
500
- name: attachment.name,
501
- url: attachment.url,
502
- proxy_url: attachment.proxyURL,
503
- size: attachment.size,
504
- content_type: attachment.contentType,
505
- },
506
- });
507
- }
508
-
509
- return segments;
510
- }
511
-
512
- async $sendMessage(options: SendOptions): Promise<string> {
513
- try {
514
- const channel = await this.channels.fetch(options.id);
515
- if (!channel || !channel.isTextBased()) {
516
- throw new Error(`Channel ${options.id} is not a text channel`);
517
- }
518
-
519
- const result = await this.sendContentToChannel(
520
- channel as any,
521
- options.content
522
- );
523
- this.pluginLogger.debug(
524
- `${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`
525
- );
526
- return result.id;
527
- } catch (error) {
528
- this.pluginLogger.error("Failed to send Discord message:", error);
529
- throw error;
530
- }
531
- }
532
-
533
- // 发送内容到频道
534
- async sendContentToChannel(
535
- channel: TextChannel | DMChannel | NewsChannel | ThreadChannel,
536
- content: SendContent,
537
- extraOptions: MessageCreateOptions = {}
538
- ): Promise<DiscordMessage<boolean>> {
539
- if (!Array.isArray(content)) content = [content];
540
-
541
- const messageOptions: MessageCreateOptions = { ...extraOptions };
542
- let textContent = "";
543
- const embeds: EmbedBuilder[] = [];
544
- const files: AttachmentBuilder[] = [];
545
-
546
- for (const segment of content) {
547
- if (typeof segment === "string") {
548
- textContent += segment;
549
- continue;
550
- }
551
-
552
- const { type, data } = segment;
553
-
554
- switch (type) {
555
- case "text":
556
- textContent += data.text || "";
557
- break;
558
-
559
- case "at":
560
- textContent += `<@${data.id}>`;
561
- break;
562
-
563
- case "channel_mention":
564
- textContent += `<#${data.id}>`;
565
- break;
566
-
567
- case "role_mention":
568
- textContent += `<@&${data.id}>`;
569
- break;
570
-
571
- case "emoji":
572
- textContent += data.animated
573
- ? `<a:${data.name}:${data.id}>`
574
- : `<:${data.name}:${data.id}>`;
575
- break;
576
-
577
- case "image":
578
- case "audio":
579
- case "video":
580
- case "file":
581
- await this.handleFileSegment(data, files, textContent);
582
- break;
583
-
584
- case "embed":
585
- embeds.push(this.createEmbedFromData(data));
586
- break;
587
-
588
- default:
589
- // 未知类型作为文本处理
590
- textContent += data.text || `[${type}]`;
591
- }
592
- }
593
-
594
- // 设置消息内容
595
- if (textContent.trim()) {
596
- messageOptions.content = textContent.trim();
597
- }
598
-
599
- if (embeds.length > 0) {
600
- messageOptions.embeds = embeds.slice(0, 10); // Discord 限制最多10个embed
601
- }
602
-
603
- if (files.length > 0) {
604
- messageOptions.files = files;
605
- }
606
-
607
- // 发送消息
608
- return await channel.send(messageOptions);
609
- }
610
- async $recallMessage(id: string): Promise<void> { }
611
-
612
- // ==================== 服务器管理 API ====================
613
-
614
- /**
615
- * 踢出成员
616
- * @param guildId 服务器 ID
617
- * @param userId 用户 ID
618
- * @param reason 原因
619
- */
620
- async kickMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
621
- try {
622
- const guild = await this.guilds.fetch(guildId);
623
- const member = await guild.members.fetch(userId);
624
- await member.kick(reason);
625
- this.pluginLogger.info(`Discord Bot ${this.$id} 踢出成员 ${userId}(服务器 ${guildId})`);
626
- return true;
627
- } catch (error) {
628
- this.pluginLogger.error(`Discord Bot ${this.$id} 踢出成员失败:`, error);
629
- throw error;
630
- }
631
- }
632
-
633
- /**
634
- * 封禁成员
635
- * @param guildId 服务器 ID
636
- * @param userId 用户 ID
637
- * @param reason 原因
638
- * @param deleteMessageDays 删除消息天数
639
- */
640
- async banMember(guildId: string, userId: string, reason?: string, deleteMessageDays?: number): Promise<boolean> {
641
- try {
642
- const guild = await this.guilds.fetch(guildId);
643
- await guild.members.ban(userId, { reason, deleteMessageSeconds: deleteMessageDays ? deleteMessageDays * 86400 : undefined });
644
- this.pluginLogger.info(`Discord Bot ${this.$id} 封禁成员 ${userId}(服务器 ${guildId})`);
645
- return true;
646
- } catch (error) {
647
- this.pluginLogger.error(`Discord Bot ${this.$id} 封禁成员失败:`, error);
648
- throw error;
649
- }
650
- }
651
-
652
- /**
653
- * 解除封禁
654
- * @param guildId 服务器 ID
655
- * @param userId 用户 ID
656
- * @param reason 原因
657
- */
658
- async unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
659
- try {
660
- const guild = await this.guilds.fetch(guildId);
661
- await guild.members.unban(userId, reason);
662
- this.pluginLogger.info(`Discord Bot ${this.$id} 解除封禁 ${userId}(服务器 ${guildId})`);
663
- return true;
664
- } catch (error) {
665
- this.pluginLogger.error(`Discord Bot ${this.$id} 解除封禁失败:`, error);
666
- throw error;
667
- }
668
- }
669
-
670
- /**
671
- * 超时(禁言)成员
672
- * @param guildId 服务器 ID
673
- * @param userId 用户 ID
674
- * @param duration 超时时长(秒),0 表示取消超时
675
- * @param reason 原因
676
- */
677
- async timeoutMember(guildId: string, userId: string, duration: number = 600, reason?: string): Promise<boolean> {
678
- try {
679
- const guild = await this.guilds.fetch(guildId);
680
- const member = await guild.members.fetch(userId);
681
- if (duration === 0) {
682
- await member.timeout(null, reason);
683
- this.pluginLogger.info(`Discord Bot ${this.$id} 取消成员 ${userId} 超时(服务器 ${guildId})`);
684
- } else {
685
- await member.timeout(duration * 1000, reason);
686
- this.pluginLogger.info(`Discord Bot ${this.$id} 超时成员 ${userId} ${duration}秒(服务器 ${guildId})`);
687
- }
688
- return true;
689
- } catch (error) {
690
- this.pluginLogger.error(`Discord Bot ${this.$id} 超时操作失败:`, error);
691
- throw error;
692
- }
693
- }
694
-
695
- /**
696
- * 修改成员昵称
697
- * @param guildId 服务器 ID
698
- * @param userId 用户 ID
699
- * @param nickname 新昵称
700
- */
701
- async setNickname(guildId: string, userId: string, nickname: string): Promise<boolean> {
702
- try {
703
- const guild = await this.guilds.fetch(guildId);
704
- const member = await guild.members.fetch(userId);
705
- await member.setNickname(nickname);
706
- this.pluginLogger.info(`Discord Bot ${this.$id} 设置成员 ${userId} 昵称为 "${nickname}"(服务器 ${guildId})`);
707
- return true;
708
- } catch (error) {
709
- this.pluginLogger.error(`Discord Bot ${this.$id} 设置昵称失败:`, error);
710
- throw error;
711
- }
712
- }
713
-
714
- /**
715
- * 添加角色
716
- * @param guildId 服务器 ID
717
- * @param userId 用户 ID
718
- * @param roleId 角色 ID
719
- */
720
- async addRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
721
- try {
722
- const guild = await this.guilds.fetch(guildId);
723
- const member = await guild.members.fetch(userId);
724
- await member.roles.add(roleId);
725
- this.pluginLogger.info(`Discord Bot ${this.$id} 给成员 ${userId} 添加角色 ${roleId}(服务器 ${guildId})`);
726
- return true;
727
- } catch (error) {
728
- this.pluginLogger.error(`Discord Bot ${this.$id} 添加角色失败:`, error);
729
- throw error;
730
- }
731
- }
732
-
733
- /**
734
- * 移除角色
735
- * @param guildId 服务器 ID
736
- * @param userId 用户 ID
737
- * @param roleId 角色 ID
738
- */
739
- async removeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
740
- try {
741
- const guild = await this.guilds.fetch(guildId);
742
- const member = await guild.members.fetch(userId);
743
- await member.roles.remove(roleId);
744
- this.pluginLogger.info(`Discord Bot ${this.$id} 移除成员 ${userId} 的角色 ${roleId}(服务器 ${guildId})`);
745
- return true;
746
- } catch (error) {
747
- this.pluginLogger.error(`Discord Bot ${this.$id} 移除角色失败:`, error);
748
- throw error;
749
- }
750
- }
751
-
752
- /**
753
- * 获取服务器角色列表
754
- * @param guildId 服务器 ID
755
- */
756
- async getRoles(guildId: string): Promise<any[]> {
757
- try {
758
- const guild = await this.guilds.fetch(guildId);
759
- await guild.roles.fetch();
760
- return guild.roles.cache.map(role => ({
761
- id: role.id,
762
- name: role.name,
763
- color: role.hexColor,
764
- position: role.position,
765
- permissions: role.permissions.bitfield.toString(),
766
- }));
767
- } catch (error) {
768
- this.pluginLogger.error(`Discord Bot ${this.$id} 获取角色列表失败:`, error);
769
- throw error;
770
- }
771
- }
772
-
773
- /**
774
- * 获取成员列表
775
- * @param guildId 服务器 ID
776
- * @param limit 数量限制
777
- */
778
- async getMembers(guildId: string, limit: number = 100): Promise<any[]> {
779
- try {
780
- const guild = await this.guilds.fetch(guildId);
781
- const members = await guild.members.fetch({ limit });
782
- return Array.from(members.values()).map(member => ({
783
- id: member.id,
784
- username: member.user.username,
785
- nickname: member.nickname,
786
- roles: member.roles.cache.map(r => r.id),
787
- joined_at: member.joinedAt?.toISOString(),
788
- }));
789
- } catch (error) {
790
- this.pluginLogger.error(`Discord Bot ${this.$id} 获取成员列表失败:`, error);
791
- throw error;
792
- }
793
- }
794
-
795
- /**
796
- * 获取服务器信息
797
- * @param guildId 服务器 ID
798
- */
799
- async getGuildInfo(guildId: string): Promise<any> {
800
- try {
801
- const guild = await this.guilds.fetch(guildId);
802
- return {
803
- id: guild.id,
804
- name: guild.name,
805
- icon: guild.iconURL(),
806
- owner_id: guild.ownerId,
807
- member_count: guild.memberCount,
808
- created_at: guild.createdAt?.toISOString(),
809
- };
810
- } catch (error) {
811
- this.pluginLogger.error(`Discord Bot ${this.$id} 获取服务器信息失败:`, error);
812
- throw error;
813
- }
814
- }
815
-
816
- async createThread(channelId: string, name: string, messageId?: string, autoArchiveDuration?: number): Promise<ThreadChannel> {
817
- try {
818
- const channel = await this.channels.fetch(channelId);
819
- if (!channel || !('threads' in channel)) throw new Error(`Channel ${channelId} 不支持创建帖子`);
820
- const options: any = { name, autoArchiveDuration: autoArchiveDuration || 1440 };
821
- if (messageId) options.startMessage = messageId;
822
- const thread = await (channel as TextChannel).threads.create(options);
823
- this.pluginLogger.info(`Discord Bot ${this.$id} 创建帖子 "${name}" (channel ${channelId})`);
824
- return thread;
825
- } catch (error) {
826
- this.pluginLogger.error(`Discord Bot ${this.$id} 创建帖子失败:`, error);
827
- throw error;
828
- }
829
- }
830
-
831
- async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
832
- try {
833
- const channel = await this.channels.fetch(channelId);
834
- if (!channel || !channel.isTextBased()) throw new Error(`Channel ${channelId} 不是文本频道`);
835
- const message = await (channel as TextChannel).messages.fetch(messageId);
836
- await message.react(emoji);
837
- this.pluginLogger.info(`Discord Bot ${this.$id} 添加反应 ${emoji} (message ${messageId})`);
838
- } catch (error) {
839
- this.pluginLogger.error(`Discord Bot ${this.$id} 添加反应失败:`, error);
840
- throw error;
841
- }
842
- }
843
-
844
- async sendEmbed(channelId: string, embedData: { title?: string; description?: string; color?: number; url?: string; fields?: { name: string; value: string; inline?: boolean }[] }): Promise<DiscordMessage<boolean>> {
845
- try {
846
- const channel = await this.channels.fetch(channelId);
847
- if (!channel || !channel.isTextBased()) throw new Error(`Channel ${channelId} 不是文本频道`);
848
- const embed = this.createEmbedFromData(embedData);
849
- const msg = await (channel as TextChannel).send({ embeds: [embed] });
850
- this.pluginLogger.info(`Discord Bot ${this.$id} 发送 Embed 到 ${channelId}`);
851
- return msg;
852
- } catch (error) {
853
- this.pluginLogger.error(`Discord Bot ${this.$id} 发送 Embed 失败:`, error);
854
- throw error;
855
- }
856
- }
857
-
858
- async createForumPost(channelId: string, name: string, content: string, tags?: string[]): Promise<ThreadChannel> {
859
- try {
860
- const channel = await this.channels.fetch(channelId);
861
- if (!channel || channel.type !== ChannelType.GuildForum) throw new Error(`Channel ${channelId} 不是论坛频道`);
862
- const forumChannel = channel as any;
863
- const options: any = {
864
- name,
865
- message: { content },
866
- };
867
- if (tags?.length && forumChannel.availableTags?.length) {
868
- const tagIds = forumChannel.availableTags
869
- .filter((t: any) => tags.includes(t.name))
870
- .map((t: any) => t.id);
871
- if (tagIds.length) options.appliedTags = tagIds;
872
- }
873
- const thread = await forumChannel.threads.create(options);
874
- this.pluginLogger.info(`Discord Bot ${this.$id} 创建论坛帖 "${name}" (channel ${channelId})`);
875
- return thread;
876
- } catch (error) {
877
- this.pluginLogger.error(`Discord Bot ${this.$id} 创建论坛帖失败:`, error);
878
- throw error;
879
- }
880
- }
881
-
882
- // 处理文件段
883
- async handleFileSegment(
884
- data: any,
885
- files: AttachmentBuilder[],
886
- textContent: string
887
- ): Promise<void> {
888
- if (data.file && (await this.fileExists(data.file))) {
889
- // 本地文件
890
- files.push(
891
- new AttachmentBuilder(createReadStream(data.file), {
892
- name: data.name || path.basename(data.file),
893
- })
894
- );
895
- } else if (data.url) {
896
- // URL 文件
897
- files.push(
898
- new AttachmentBuilder(data.url, {
899
- name: data.name || "attachment",
900
- })
901
- );
902
- } else if (data.buffer) {
903
- // Buffer 数据
904
- files.push(
905
- new AttachmentBuilder(data.buffer, {
906
- name: data.name || "attachment",
907
- })
908
- );
909
- }
910
- }
911
-
912
- // 从数据创建 Embed
913
- createEmbedFromData(data: any): EmbedBuilder {
914
- const embed = new EmbedBuilder();
915
-
916
- if (data.title) embed.setTitle(data.title);
917
- if (data.description) embed.setDescription(data.description);
918
- if (data.color) embed.setColor(data.color);
919
- if (data.url) embed.setURL(data.url);
920
- if (data.thumbnail?.url) embed.setThumbnail(data.thumbnail.url);
921
- if (data.image?.url) embed.setImage(data.image.url);
922
- if (data.author) embed.setAuthor(data.author);
923
- if (data.footer) embed.setFooter(data.footer);
924
- if (data.timestamp) embed.setTimestamp(new Date(data.timestamp));
925
- if (data.fields && Array.isArray(data.fields)) {
926
- embed.addFields(data.fields);
927
- }
928
-
929
- return embed;
930
- }
931
-
932
- // 工具方法:获取活动类型
933
- private getActivityType(type: string) {
934
- const activityTypes = {
935
- PLAYING: 0,
936
- STREAMING: 1,
937
- LISTENING: 2,
938
- WATCHING: 3,
939
- COMPETING: 5,
940
- };
941
- return activityTypes[type as keyof typeof activityTypes] || 0;
942
- }
943
-
944
- // 注册 Slash Commands
945
- private async registerSlashCommands(): Promise<void> {
946
- if (!this.$config.slashCommands || !this.user) return;
947
-
948
- try {
949
- const rest = new REST({ version: "10" }).setToken(this.$config.token);
950
-
951
- if (this.$config.globalCommands) {
952
- // 注册全局命令
953
- await rest.put(Routes.applicationCommands(this.user.id), {
954
- body: this.$config.slashCommands,
955
- });
956
- this.pluginLogger.info("Successfully registered global slash commands");
957
- } else {
958
- // 为每个服务器注册命令
959
- for (const guild of this.guilds.cache.values()) {
960
- await rest.put(
961
- Routes.applicationGuildCommands(this.user.id, guild.id),
962
- { body: this.$config.slashCommands }
963
- );
964
- }
965
- this.pluginLogger.info("Successfully registered guild slash commands");
966
- }
967
- } catch (error) {
968
- this.pluginLogger.error("Failed to register slash commands:", error);
969
- }
970
- }
971
-
972
- // 添加 Slash Command 处理器
973
- addSlashCommandHandler(
974
- commandName: string,
975
- handler: (interaction: ChatInputCommandInteraction) => Promise<void>
976
- ) {
977
- this.slashCommandHandlers.set(commandName, handler);
978
- }
979
-
980
- // 移除 Slash Command 处理器
981
- removeSlashCommandHandler(commandName: string): boolean {
982
- return this.slashCommandHandlers.delete(commandName);
983
- }
984
-
985
- // 工具方法:检查文件是否存在
986
- private async fileExists(filePath: string): Promise<boolean> {
987
- try {
988
- await fs.access(filePath);
989
- return true;
990
- } catch {
991
- return false;
992
- }
993
- }
994
-
995
- // 静态方法:格式化内容为文本(用于日志显示)
996
- static formatContentToText(content: SendContent): string {
997
- if (!Array.isArray(content)) content = [content];
998
-
999
- return content
1000
- .map((segment) => {
1001
- if (typeof segment === "string") return segment;
1002
-
1003
- switch (segment.type) {
1004
- case "text":
1005
- return segment.data.text || "";
1006
- case "at":
1007
- return `@${segment.data.name || segment.data.id}`;
1008
- case "channel_mention":
1009
- return `#${segment.data.name}`;
1010
- case "role_mention":
1011
- return `@${segment.data.name}`;
1012
- case "image":
1013
- return "[图片]";
1014
- case "audio":
1015
- return "[音频]";
1016
- case "video":
1017
- return "[视频]";
1018
- case "file":
1019
- return "[文件]";
1020
- case "embed":
1021
- return "[嵌入消息]";
1022
- case "emoji":
1023
- return `:${segment.data.name}:`;
1024
- default:
1025
- return `[${segment.type}]`;
1026
- }
1027
- })
1028
- .join("");
1029
- }
1030
- }
1031
-
1032
- // ================================================================================================
1033
- // DiscordInteractionsBot 类(Interactions 端点模式)
1034
- // ================================================================================================
1035
-
1036
- import * as nacl from "tweetnacl";
1037
-