@zhin.js/adapter-discord 1.0.87 → 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.
- package/CHANGELOG.md +855 -66
- package/README.md +61 -67
- package/adapters/discord.js +41 -0
- package/adapters/discord.ts +57 -0
- package/agent/PERMITS.md +25 -0
- package/{skills/discord/SKILL.md → agent/skills/discord.md} +1 -1
- package/agent/tools/add_role.ts +26 -0
- package/agent/tools/create_thread.ts +28 -0
- package/agent/tools/forum_post.ts +33 -0
- package/agent/tools/list_roles.ts +34 -0
- package/agent/tools/react.ts +24 -0
- package/agent/tools/remove_role.ts +26 -0
- package/agent/tools/send_embed.ts +38 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +16 -0
- package/lib/client.js +8 -0
- package/lib/discord-endpoint-commands.d.ts +1 -0
- package/lib/discord-endpoint-commands.js +18 -0
- package/lib/discord-runtime-state.d.ts +1 -0
- package/lib/discord-runtime-state.js +6 -0
- package/lib/endpoint.d.ts +72 -0
- package/lib/endpoint.js +458 -0
- package/lib/gateway.d.ts +144 -0
- package/lib/gateway.js +303 -0
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -323
- package/lib/platform-permit.d.ts +14 -0
- package/lib/platform-permit.js +42 -0
- package/lib/protocol.d.ts +162 -0
- package/lib/protocol.js +333 -0
- package/lib/side-event-dispatch.d.ts +8 -0
- package/lib/side-event-dispatch.js +24 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +88 -0
- package/package.json +62 -29
- package/plugin.js +19 -0
- package/schema.json +141 -0
- package/src/client.ts +24 -0
- package/src/discord-endpoint-commands.ts +19 -0
- package/src/discord-runtime-state.ts +7 -0
- package/src/endpoint.ts +562 -0
- package/src/gateway.ts +426 -0
- package/src/index.ts +55 -323
- package/src/platform-permit.ts +57 -0
- package/src/protocol.ts +502 -0
- package/src/side-event-dispatch.ts +37 -0
- package/src/webhook.ts +123 -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 -17
- package/dist/index.js +0 -29
- package/lib/adapter.d.ts +0 -19
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -90
- package/lib/adapter.js.map +0 -1
- package/lib/bot-interactions.d.ts +0 -33
- package/lib/bot-interactions.d.ts.map +0 -1
- package/lib/bot-interactions.js +0 -278
- package/lib/bot-interactions.js.map +0 -1
- package/lib/bot.d.ts +0 -117
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -904
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.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 -107
- package/src/bot-interactions.ts +0 -348
- package/src/bot.ts +0 -1037
- package/src/types.ts +0 -60
package/lib/gateway.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Client, GatewayIntentBits, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, REST, Routes, PermissionFlagsBits, } from 'discord.js';
|
|
4
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
5
|
+
import { activityTypeCode, resolveChannelKind, } from './protocol.js';
|
|
6
|
+
const logger = getLogger('discord');
|
|
7
|
+
export const DEFAULT_INTENTS = [
|
|
8
|
+
GatewayIntentBits.Guilds,
|
|
9
|
+
GatewayIntentBits.GuildMessages,
|
|
10
|
+
GatewayIntentBits.MessageContent,
|
|
11
|
+
GatewayIntentBits.DirectMessages,
|
|
12
|
+
GatewayIntentBits.GuildMembers,
|
|
13
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
14
|
+
];
|
|
15
|
+
export function defaultCreateClient(intents) {
|
|
16
|
+
return new Client({ intents: [...intents] });
|
|
17
|
+
}
|
|
18
|
+
export function resolveSenderRole(msg) {
|
|
19
|
+
if (msg.isGuildOwner)
|
|
20
|
+
return 'owner';
|
|
21
|
+
const tokens = msg.permissionTokens ?? [];
|
|
22
|
+
if (tokens.includes('ADMINISTRATOR') || tokens.includes('MODERATE_MEMBERS'))
|
|
23
|
+
return 'admin';
|
|
24
|
+
if (msg.guildId)
|
|
25
|
+
return 'member';
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeDiscordMessage(raw) {
|
|
29
|
+
if (!raw || typeof raw !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const msg = raw;
|
|
32
|
+
if (!msg.author || !msg.channel)
|
|
33
|
+
return null;
|
|
34
|
+
const permissionTokens = [];
|
|
35
|
+
let isGuildOwner = false;
|
|
36
|
+
const member = msg.member;
|
|
37
|
+
const guild = msg.guild;
|
|
38
|
+
if (member && guild) {
|
|
39
|
+
const checks = [
|
|
40
|
+
[PermissionFlagsBits.Administrator, 'ADMINISTRATOR'],
|
|
41
|
+
[PermissionFlagsBits.ManageRoles, 'MANAGE_ROLES'],
|
|
42
|
+
[PermissionFlagsBits.ModerateMembers, 'MODERATE_MEMBERS'],
|
|
43
|
+
[PermissionFlagsBits.ManageChannels, 'MANAGE_CHANNELS'],
|
|
44
|
+
[PermissionFlagsBits.ManageGuild, 'MANAGE_GUILD'],
|
|
45
|
+
];
|
|
46
|
+
for (const [bit, name] of checks) {
|
|
47
|
+
if (member.permissions.has(bit))
|
|
48
|
+
permissionTokens.push(name);
|
|
49
|
+
}
|
|
50
|
+
if (guild.ownerId === msg.author.id) {
|
|
51
|
+
isGuildOwner = true;
|
|
52
|
+
permissionTokens.push('guild_owner', 'ADMINISTRATOR');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: msg.id,
|
|
57
|
+
content: msg.content ?? '',
|
|
58
|
+
channelId: msg.channel.id,
|
|
59
|
+
channelKind: resolveChannelKind(msg.channel.type),
|
|
60
|
+
authorId: msg.author.id,
|
|
61
|
+
authorName: member?.displayName || msg.author.displayName || msg.author.username,
|
|
62
|
+
authorBot: msg.author.bot,
|
|
63
|
+
createdTimestamp: msg.createdTimestamp,
|
|
64
|
+
guildId: guild?.id,
|
|
65
|
+
isGuildOwner,
|
|
66
|
+
permissionTokens,
|
|
67
|
+
attachments: [...msg.attachments.values()].map((a) => ({
|
|
68
|
+
id: a.id,
|
|
69
|
+
name: a.name ?? undefined,
|
|
70
|
+
url: a.url,
|
|
71
|
+
contentType: a.contentType ?? undefined,
|
|
72
|
+
size: a.size,
|
|
73
|
+
})),
|
|
74
|
+
embedTitles: msg.embeds.map((e) => e.title || e.description || 'embed').filter(Boolean),
|
|
75
|
+
stickerNames: [...msg.stickers.values()].map((s) => s.name),
|
|
76
|
+
replyToId: msg.reference?.messageId ?? undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function toMessageCreateOptions(body) {
|
|
80
|
+
const options = {};
|
|
81
|
+
if (body.content)
|
|
82
|
+
options.content = body.content;
|
|
83
|
+
if (body.embeds?.length) {
|
|
84
|
+
options.embeds = body.embeds.map((data) => {
|
|
85
|
+
const embed = new EmbedBuilder();
|
|
86
|
+
if (data.title)
|
|
87
|
+
embed.setTitle(String(data.title));
|
|
88
|
+
if (data.description)
|
|
89
|
+
embed.setDescription(String(data.description));
|
|
90
|
+
if (data.color != null)
|
|
91
|
+
embed.setColor(data.color);
|
|
92
|
+
if (data.url)
|
|
93
|
+
embed.setURL(String(data.url));
|
|
94
|
+
const thumb = data.thumbnail;
|
|
95
|
+
if (thumb?.url)
|
|
96
|
+
embed.setThumbnail(thumb.url);
|
|
97
|
+
const image = data.image;
|
|
98
|
+
if (image?.url)
|
|
99
|
+
embed.setImage(image.url);
|
|
100
|
+
if (data.author)
|
|
101
|
+
embed.setAuthor(data.author);
|
|
102
|
+
if (data.footer)
|
|
103
|
+
embed.setFooter(data.footer);
|
|
104
|
+
if (data.timestamp)
|
|
105
|
+
embed.setTimestamp(new Date(String(data.timestamp)));
|
|
106
|
+
if (Array.isArray(data.fields))
|
|
107
|
+
embed.addFields(data.fields);
|
|
108
|
+
return embed;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (body.files?.length) {
|
|
112
|
+
const files = [];
|
|
113
|
+
for (const file of body.files) {
|
|
114
|
+
if (file.base64) {
|
|
115
|
+
files.push(new AttachmentBuilder(decodeBase64(file.base64), {
|
|
116
|
+
name: file.name || 'attachment',
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
else if (file.file && await fileExists(file.file)) {
|
|
120
|
+
files.push(new AttachmentBuilder(createReadStream(file.file), {
|
|
121
|
+
name: file.name || path.basename(file.file),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
else if (file.url) {
|
|
125
|
+
files.push(new AttachmentBuilder(file.url, { name: file.name || 'attachment' }));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
logger.warn(formatCompact({
|
|
129
|
+
op: 'discord_outbound_media_dropped',
|
|
130
|
+
reason: 'missing_source',
|
|
131
|
+
name: file.name,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (files.length)
|
|
136
|
+
options.files = files;
|
|
137
|
+
}
|
|
138
|
+
if (body.components?.length) {
|
|
139
|
+
options.components = body.components.map((row) => new ActionRowBuilder().addComponents(...row.components.map((btn) => {
|
|
140
|
+
const b = new ButtonBuilder()
|
|
141
|
+
.setCustomId(btn.custom_id)
|
|
142
|
+
.setLabel(btn.label)
|
|
143
|
+
.setDisabled(!!btn.disabled);
|
|
144
|
+
if (btn.style === 4)
|
|
145
|
+
b.setStyle(ButtonStyle.Danger);
|
|
146
|
+
else if (btn.style === 1)
|
|
147
|
+
b.setStyle(ButtonStyle.Primary);
|
|
148
|
+
else
|
|
149
|
+
b.setStyle(ButtonStyle.Secondary);
|
|
150
|
+
return b;
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
return options;
|
|
154
|
+
}
|
|
155
|
+
async function registerSlashCommands(config, applicationId, guildIds = []) {
|
|
156
|
+
if (!config.slashCommands?.length)
|
|
157
|
+
return;
|
|
158
|
+
const rest = new REST({ version: '10' }).setToken(config.token);
|
|
159
|
+
if (config.globalCommands) {
|
|
160
|
+
await rest.put(Routes.applicationCommands(applicationId), {
|
|
161
|
+
body: config.slashCommands,
|
|
162
|
+
});
|
|
163
|
+
logger.info(formatCompact({ op: 'slash_commands', scope: 'global' }));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// globalCommands=false 时按 guild 注册,否则 enableSlashCommands 会是静默 no-op
|
|
167
|
+
if (guildIds.length === 0) {
|
|
168
|
+
logger.warn(formatCompact({
|
|
169
|
+
op: 'slash_commands',
|
|
170
|
+
ok: false,
|
|
171
|
+
error: 'enableSlashCommands=true but globalCommands=false and no guilds cached; slash commands not registered',
|
|
172
|
+
}));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const guildId of guildIds) {
|
|
176
|
+
await rest.put(Routes.applicationGuildCommands(applicationId, guildId), {
|
|
177
|
+
body: config.slashCommands,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
logger.info(formatCompact({ op: 'slash_commands', scope: 'guild', guilds: guildIds.length }));
|
|
181
|
+
}
|
|
182
|
+
async function fileExists(filePath) {
|
|
183
|
+
try {
|
|
184
|
+
await fs.access(filePath);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** base64:// / data:*;base64, 前缀归一,Buffer.from 只接受纯 base64。 */
|
|
192
|
+
function decodeBase64(value) {
|
|
193
|
+
const stripped = value.startsWith('base64://') ? value.slice('base64://'.length) : value;
|
|
194
|
+
const comma = stripped.startsWith('data:') ? stripped.indexOf(',') : -1;
|
|
195
|
+
return Buffer.from(comma >= 0 ? stripped.slice(comma + 1) : stripped, 'base64');
|
|
196
|
+
}
|
|
197
|
+
export async function connectDiscordGatewayClient(client, config, handlers) {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
let settled = false;
|
|
200
|
+
client.on('messageCreate', (raw) => {
|
|
201
|
+
handlers.onPlatformEvent('messageCreate', raw);
|
|
202
|
+
const msg = normalizeDiscordMessage(raw);
|
|
203
|
+
if (!msg)
|
|
204
|
+
return;
|
|
205
|
+
// clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
|
|
206
|
+
const botId = client.user?.id;
|
|
207
|
+
const mentions = raw.mentions;
|
|
208
|
+
const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
|
|
209
|
+
handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
|
|
210
|
+
});
|
|
211
|
+
client.on('interactionCreate', (raw) => {
|
|
212
|
+
handlers.onPlatformEvent('interactionCreate', raw);
|
|
213
|
+
const interaction = raw;
|
|
214
|
+
if (!interaction.isButton?.())
|
|
215
|
+
return;
|
|
216
|
+
void interaction.deferUpdate?.().catch(() => { });
|
|
217
|
+
if (!interaction.channel)
|
|
218
|
+
return;
|
|
219
|
+
handlers.onButton({
|
|
220
|
+
id: interaction.id,
|
|
221
|
+
customId: interaction.customId,
|
|
222
|
+
channelId: interaction.channel.id,
|
|
223
|
+
channelKind: resolveChannelKind(interaction.channel.type),
|
|
224
|
+
userId: interaction.user.id,
|
|
225
|
+
userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
|
|
226
|
+
sourceMessageId: interaction.message?.id,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
client.on('guildMemberAdd', (raw) => {
|
|
230
|
+
handlers.onPlatformEvent('guildMemberAdd', raw);
|
|
231
|
+
const member = raw;
|
|
232
|
+
const guildId = member.guild?.id;
|
|
233
|
+
const userId = member.user?.id;
|
|
234
|
+
if (!guildId || !userId)
|
|
235
|
+
return;
|
|
236
|
+
handlers.onGuildMemberAdd?.({
|
|
237
|
+
guildId,
|
|
238
|
+
userId,
|
|
239
|
+
userName: member.user?.username || member.user?.displayName,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
client.on('guildMemberRemove', (raw) => {
|
|
243
|
+
handlers.onPlatformEvent('guildMemberRemove', raw);
|
|
244
|
+
const member = raw;
|
|
245
|
+
const guildId = member.guild?.id;
|
|
246
|
+
const userId = member.user?.id;
|
|
247
|
+
if (!guildId || !userId)
|
|
248
|
+
return;
|
|
249
|
+
handlers.onGuildMemberRemove?.({
|
|
250
|
+
guildId,
|
|
251
|
+
userId,
|
|
252
|
+
userName: member.user?.username || member.user?.displayName,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
client.once('clientReady', () => {
|
|
256
|
+
handlers.onPlatformEvent('clientReady', client.user);
|
|
257
|
+
void (async () => {
|
|
258
|
+
try {
|
|
259
|
+
if (config.defaultActivity && client.user?.setActivity) {
|
|
260
|
+
client.user.setActivity(config.defaultActivity.name, {
|
|
261
|
+
type: activityTypeCode(config.defaultActivity.type),
|
|
262
|
+
url: config.defaultActivity.url,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (config.enableSlashCommands && config.slashCommands?.length && client.user) {
|
|
266
|
+
const guildIds = [...client.guilds.cache.values()].map((guild) => guild.id);
|
|
267
|
+
await registerSlashCommands(config, client.user.id, guildIds);
|
|
268
|
+
}
|
|
269
|
+
if (!settled) {
|
|
270
|
+
settled = true;
|
|
271
|
+
resolve();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (!settled) {
|
|
276
|
+
settled = true;
|
|
277
|
+
reject(error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
});
|
|
282
|
+
client.on('error', (error) => {
|
|
283
|
+
logger.error('Discord client error:', error);
|
|
284
|
+
if (!settled) {
|
|
285
|
+
settled = true;
|
|
286
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
client.on('warn', (info) => {
|
|
290
|
+
logger.warn('Discord client warning:', info);
|
|
291
|
+
});
|
|
292
|
+
client.login(config.token).catch((error) => {
|
|
293
|
+
if (!settled) {
|
|
294
|
+
settled = true;
|
|
295
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
296
|
+
const tokenShapeHint = config.token.split('.').length === 3
|
|
297
|
+
? ''
|
|
298
|
+
: ';当前 token 不是标准三段式 Bot Token 格式,请到 Discord Developer Portal → Bot → Reset Token 重新获取(勿用 Client Secret)';
|
|
299
|
+
reject(new Error(`Discord 连接失败:请检查 token 是否为 Bot Token 且未过期、Bot 是否已开启所需 Intents${tokenShapeHint}(原始错误:${raw})`, { cause: error instanceof Error ? error : undefined }));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
web: PageManager;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
interface Adapters {
|
|
11
|
-
discord: DiscordAdapter;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export * from "./types.js";
|
|
15
|
-
export { DiscordBot } from "./bot.js";
|
|
16
|
-
export { DiscordInteractionsBot } from "./bot-interactions.js";
|
|
17
|
-
export { DiscordAdapter, type DiscordBotLike } from "./adapter.js";
|
|
18
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, type DiscordAdapterConfig, type DiscordButtonInbound, type DiscordInboundAttachment, type DiscordInboundMessage, type DiscordOutboundBody, type DiscordWireSegment, type ResolvedDiscordConfig, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig, } from './protocol.js';
|
|
2
|
+
export { discordClient, type DiscordClient, type DiscordClientEventMap, } from './client.js';
|
|
3
|
+
export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
|
+
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type CreateDiscordClient, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
|
|
5
|
+
export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
|
|
6
|
+
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, type DiscordInteractionsHandler, } from './webhook.js';
|
package/lib/index.js
CHANGED
|
@@ -1,323 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export * from "./types.js";
|
|
8
|
-
export { DiscordBot } from "./bot.js";
|
|
9
|
-
export { DiscordInteractionsBot } from "./bot-interactions.js";
|
|
10
|
-
export { DiscordAdapter } from "./adapter.js";
|
|
11
|
-
const plugin = usePlugin();
|
|
12
|
-
const { provide, useContext } = plugin;
|
|
13
|
-
provide({
|
|
14
|
-
name: "discord",
|
|
15
|
-
description: "Discord 适配器(Gateway / Interactions)",
|
|
16
|
-
mounted: async (p) => {
|
|
17
|
-
const adapter = new DiscordAdapter(p);
|
|
18
|
-
await adapter.start();
|
|
19
|
-
return adapter;
|
|
20
|
-
},
|
|
21
|
-
dispose: async (adapter) => {
|
|
22
|
-
await adapter.stop();
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
useContext('tool', 'discord', (toolService, discord) => {
|
|
26
|
-
const groupTools = createGroupManagementTools(discord, 'discord');
|
|
27
|
-
const disposers = groupTools.map(t => toolService.addTool(t, plugin.name));
|
|
28
|
-
function getGatewayBot(botId) {
|
|
29
|
-
const bot = discord.bots.get(botId);
|
|
30
|
-
if (!bot)
|
|
31
|
-
throw new Error(`Bot ${botId} 不存在`);
|
|
32
|
-
if (bot.$config.connection !== 'gateway') {
|
|
33
|
-
throw new Error('此工具仅支持 connection: gateway');
|
|
34
|
-
}
|
|
35
|
-
return bot;
|
|
36
|
-
}
|
|
37
|
-
disposers.push(toolService.addTool({
|
|
38
|
-
name: 'discord_add_role',
|
|
39
|
-
description: '给成员添加 Discord 角色',
|
|
40
|
-
parameters: {
|
|
41
|
-
type: 'object',
|
|
42
|
-
properties: {
|
|
43
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
44
|
-
guild_id: { type: 'string', description: '服务器 ID' },
|
|
45
|
-
user_id: { type: 'string', description: '用户 ID' },
|
|
46
|
-
role_id: { type: 'string', description: '角色 ID' },
|
|
47
|
-
},
|
|
48
|
-
required: ['bot', 'guild_id', 'user_id', 'role_id'],
|
|
49
|
-
},
|
|
50
|
-
platforms: ['discord'],
|
|
51
|
-
tags: ['discord'],
|
|
52
|
-
execute: async (args) => {
|
|
53
|
-
const bot = getGatewayBot(args.bot);
|
|
54
|
-
const success = await bot.addRole(args.guild_id, args.user_id, args.role_id);
|
|
55
|
-
return { success, message: success ? `已给用户 ${args.user_id} 添加角色` : '操作失败' };
|
|
56
|
-
},
|
|
57
|
-
}, plugin.name));
|
|
58
|
-
disposers.push(toolService.addTool({
|
|
59
|
-
name: 'discord_remove_role',
|
|
60
|
-
description: '移除成员的 Discord 角色',
|
|
61
|
-
parameters: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
65
|
-
guild_id: { type: 'string', description: '服务器 ID' },
|
|
66
|
-
user_id: { type: 'string', description: '用户 ID' },
|
|
67
|
-
role_id: { type: 'string', description: '角色 ID' },
|
|
68
|
-
},
|
|
69
|
-
required: ['bot', 'guild_id', 'user_id', 'role_id'],
|
|
70
|
-
},
|
|
71
|
-
platforms: ['discord'],
|
|
72
|
-
tags: ['discord'],
|
|
73
|
-
execute: async (args) => {
|
|
74
|
-
const bot = getGatewayBot(args.bot);
|
|
75
|
-
const success = await bot.removeRole(args.guild_id, args.user_id, args.role_id);
|
|
76
|
-
return { success, message: success ? `已移除用户 ${args.user_id} 的角色` : '操作失败' };
|
|
77
|
-
},
|
|
78
|
-
}, plugin.name));
|
|
79
|
-
disposers.push(toolService.addTool({
|
|
80
|
-
name: 'discord_list_roles',
|
|
81
|
-
description: '获取 Discord 服务器角色列表',
|
|
82
|
-
parameters: {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
86
|
-
guild_id: { type: 'string', description: '服务器 ID' },
|
|
87
|
-
},
|
|
88
|
-
required: ['bot', 'guild_id'],
|
|
89
|
-
},
|
|
90
|
-
platforms: ['discord'],
|
|
91
|
-
tags: ['discord'],
|
|
92
|
-
execute: async (args) => {
|
|
93
|
-
const bot = getGatewayBot(args.bot);
|
|
94
|
-
const roles = await bot.getRoles(args.guild_id);
|
|
95
|
-
return { roles, count: roles.length };
|
|
96
|
-
},
|
|
97
|
-
}, plugin.name));
|
|
98
|
-
disposers.push(toolService.addTool({
|
|
99
|
-
name: 'discord_create_thread',
|
|
100
|
-
description: '在 Discord 频道中创建帖子/子线程',
|
|
101
|
-
parameters: {
|
|
102
|
-
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
105
|
-
channel_id: { type: 'string', description: '频道 ID' },
|
|
106
|
-
name: { type: 'string', description: '帖子标题' },
|
|
107
|
-
message_id: { type: 'string', description: '基于某条消息创建(可选)' },
|
|
108
|
-
auto_archive_duration: {
|
|
109
|
-
type: 'number',
|
|
110
|
-
description: '自动归档时间(分钟:60/1440/4320/10080)',
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
required: ['bot', 'channel_id', 'name'],
|
|
114
|
-
},
|
|
115
|
-
platforms: ['discord'],
|
|
116
|
-
tags: ['discord'],
|
|
117
|
-
execute: async (args) => {
|
|
118
|
-
const bot = getGatewayBot(args.bot);
|
|
119
|
-
const thread = await bot.createThread(args.channel_id, args.name, args.message_id, args.auto_archive_duration);
|
|
120
|
-
return { success: true, thread_id: thread.id, message: `帖子 "${args.name}" 已创建` };
|
|
121
|
-
},
|
|
122
|
-
}, plugin.name));
|
|
123
|
-
disposers.push(toolService.addTool({
|
|
124
|
-
name: 'discord_react',
|
|
125
|
-
description: '对 Discord 消息添加表情反应',
|
|
126
|
-
parameters: {
|
|
127
|
-
type: 'object',
|
|
128
|
-
properties: {
|
|
129
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
130
|
-
channel_id: { type: 'string', description: '频道 ID' },
|
|
131
|
-
message_id: { type: 'string', description: '消息 ID' },
|
|
132
|
-
emoji: {
|
|
133
|
-
type: 'string',
|
|
134
|
-
description: '表情(Unicode 表情或自定义表情如 <:name:id>)',
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
required: ['bot', 'channel_id', 'message_id', 'emoji'],
|
|
138
|
-
},
|
|
139
|
-
platforms: ['discord'],
|
|
140
|
-
tags: ['discord'],
|
|
141
|
-
execute: async (args) => {
|
|
142
|
-
const bot = getGatewayBot(args.bot);
|
|
143
|
-
await bot.addReaction(args.channel_id, args.message_id, args.emoji);
|
|
144
|
-
return { success: true, message: `已添加反应 ${args.emoji}` };
|
|
145
|
-
},
|
|
146
|
-
}, plugin.name));
|
|
147
|
-
disposers.push(toolService.addTool({
|
|
148
|
-
name: 'discord_send_embed',
|
|
149
|
-
description: '发送 Discord 富文本嵌入消息(Embed)',
|
|
150
|
-
parameters: {
|
|
151
|
-
type: 'object',
|
|
152
|
-
properties: {
|
|
153
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
154
|
-
channel_id: { type: 'string', description: '频道 ID' },
|
|
155
|
-
title: { type: 'string', description: 'Embed 标题' },
|
|
156
|
-
description: { type: 'string', description: 'Embed 描述' },
|
|
157
|
-
color: { type: 'number', description: '颜色值(十进制,如 0x00ff00 = 65280)' },
|
|
158
|
-
url: { type: 'string', description: '标题链接(可选)' },
|
|
159
|
-
fields: {
|
|
160
|
-
type: 'string',
|
|
161
|
-
description: '字段,JSON 格式: [{"name":"k","value":"v","inline":false}]',
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
required: ['bot', 'channel_id'],
|
|
165
|
-
},
|
|
166
|
-
platforms: ['discord'],
|
|
167
|
-
tags: ['discord'],
|
|
168
|
-
execute: async (args) => {
|
|
169
|
-
const bot = getGatewayBot(args.bot);
|
|
170
|
-
const embedData = {};
|
|
171
|
-
if (args.title)
|
|
172
|
-
embedData.title = args.title;
|
|
173
|
-
if (args.description)
|
|
174
|
-
embedData.description = args.description;
|
|
175
|
-
if (args.color)
|
|
176
|
-
embedData.color = args.color;
|
|
177
|
-
if (args.url)
|
|
178
|
-
embedData.url = args.url;
|
|
179
|
-
if (args.fields) {
|
|
180
|
-
try {
|
|
181
|
-
embedData.fields = JSON.parse(args.fields);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
return { success: false, message: 'fields 格式错误,应为 JSON 数组' };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const msg = await bot.sendEmbed(args.channel_id, embedData);
|
|
188
|
-
return { success: true, message_id: msg.id, message: 'Embed 已发送' };
|
|
189
|
-
},
|
|
190
|
-
}, plugin.name));
|
|
191
|
-
disposers.push(toolService.addTool({
|
|
192
|
-
name: 'discord_forum_post',
|
|
193
|
-
description: '在 Discord 论坛频道中创建帖子',
|
|
194
|
-
parameters: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
198
|
-
channel_id: { type: 'string', description: '论坛频道 ID' },
|
|
199
|
-
name: { type: 'string', description: '帖子标题' },
|
|
200
|
-
content: { type: 'string', description: '帖子内容' },
|
|
201
|
-
tags: { type: 'string', description: '标签名,逗号分隔(可选)' },
|
|
202
|
-
},
|
|
203
|
-
required: ['bot', 'channel_id', 'name', 'content'],
|
|
204
|
-
},
|
|
205
|
-
platforms: ['discord'],
|
|
206
|
-
tags: ['discord'],
|
|
207
|
-
execute: async (args) => {
|
|
208
|
-
const bot = getGatewayBot(args.bot);
|
|
209
|
-
const tagList = args.tags ? args.tags.split(',').map((t) => t.trim()) : undefined;
|
|
210
|
-
const thread = await bot.createForumPost(args.channel_id, args.name, args.content, tagList);
|
|
211
|
-
return { success: true, thread_id: thread.id, message: `论坛帖 "${args.name}" 已创建` };
|
|
212
|
-
},
|
|
213
|
-
}, plugin.name));
|
|
214
|
-
return () => disposers.forEach(d => d());
|
|
215
|
-
});
|
|
216
|
-
// ── Web 控制台 ─────────────────────────────────────────────────────────
|
|
217
|
-
useContext("web", (pageManager) => {
|
|
218
|
-
pageManager.addEntry({
|
|
219
|
-
id: "discord",
|
|
220
|
-
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
221
|
-
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
222
|
-
meta: { name: "Discord" },
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
useContext("router", "discord", (router, discord) => {
|
|
226
|
-
router.get("/api/discord/bots", async (ctx) => {
|
|
227
|
-
try {
|
|
228
|
-
const bots = Array.from(discord.bots.values());
|
|
229
|
-
const result = bots.map((bot) => {
|
|
230
|
-
try {
|
|
231
|
-
const client = bot.client || bot;
|
|
232
|
-
return {
|
|
233
|
-
name: bot.$config.name,
|
|
234
|
-
connected: bot.$connected || false,
|
|
235
|
-
mode: bot.$config.connection || "gateway",
|
|
236
|
-
guildCount: client.guilds?.cache?.size || 0,
|
|
237
|
-
channelCount: client.channels?.cache?.size || 0,
|
|
238
|
-
status: bot.$connected ? "online" : "offline",
|
|
239
|
-
user: client.user ? { tag: client.user.tag, id: client.user.id } : null,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return { name: bot.$config.name, connected: false, mode: "unknown", guildCount: 0, channelCount: 0, status: "error", user: null };
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
ctx.body = { success: true, data: result };
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
ctx.status = 500;
|
|
250
|
-
ctx.body = { success: false, error: "获取机器人数据失败" };
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
// Bot 连接/断开
|
|
254
|
-
router.post("/api/discord/bots/:name/connect", async (ctx) => {
|
|
255
|
-
try {
|
|
256
|
-
const bot = discord.bots.get(ctx.params.name);
|
|
257
|
-
if (!bot) {
|
|
258
|
-
ctx.status = 404;
|
|
259
|
-
ctx.body = { success: false, error: "Bot 不存在" };
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (bot.$connected) {
|
|
263
|
-
ctx.body = { success: true, message: "已经在线" };
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
await bot.$connect();
|
|
267
|
-
ctx.body = { success: true, message: "连接成功" };
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
ctx.status = 500;
|
|
271
|
-
ctx.body = { success: false, error: e?.message || "连接失败" };
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
router.post("/api/discord/bots/:name/disconnect", async (ctx) => {
|
|
275
|
-
try {
|
|
276
|
-
const bot = discord.bots.get(ctx.params.name);
|
|
277
|
-
if (!bot) {
|
|
278
|
-
ctx.status = 404;
|
|
279
|
-
ctx.body = { success: false, error: "Bot 不存在" };
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (!bot.$connected) {
|
|
283
|
-
ctx.body = { success: true, message: "已经离线" };
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
await bot.$disconnect();
|
|
287
|
-
ctx.body = { success: true, message: "已断开" };
|
|
288
|
-
}
|
|
289
|
-
catch (e) {
|
|
290
|
-
ctx.status = 500;
|
|
291
|
-
ctx.body = { success: false, error: e?.message || "断开失败" };
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
// 服务器列表(仅 Gateway 模式)
|
|
295
|
-
router.get("/api/discord/bots/:name/guilds", async (ctx) => {
|
|
296
|
-
try {
|
|
297
|
-
const bot = discord.bots.get(ctx.params.name);
|
|
298
|
-
if (!bot) {
|
|
299
|
-
ctx.status = 404;
|
|
300
|
-
ctx.body = { success: false, error: "Bot 不存在" };
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (!bot.$connected) {
|
|
304
|
-
ctx.status = 400;
|
|
305
|
-
ctx.body = { success: false, error: "Bot 未连接" };
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const client = bot.client || bot;
|
|
309
|
-
const guilds = client.guilds?.cache?.map((g) => ({
|
|
310
|
-
id: g.id,
|
|
311
|
-
name: g.name,
|
|
312
|
-
memberCount: g.memberCount,
|
|
313
|
-
icon: g.iconURL({ size: 64 }),
|
|
314
|
-
})) || [];
|
|
315
|
-
ctx.body = { success: true, data: guilds };
|
|
316
|
-
}
|
|
317
|
-
catch (e) {
|
|
318
|
-
ctx.status = 500;
|
|
319
|
-
ctx.body = { success: false, error: e?.message || "获取服务器列表失败" };
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, } from './protocol.js';
|
|
2
|
+
export { discordClient, } from './client.js';
|
|
3
|
+
export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
|
+
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from './endpoint.js';
|
|
5
|
+
export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
|
|
6
|
+
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, } from './webhook.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord platform permit — Guild 权限位
|
|
3
|
+
*/
|
|
4
|
+
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
|
+
export declare function platformPermit(perm: string): string;
|
|
6
|
+
export declare function discordGroupPermitResolver(logicalPerm: string): string;
|
|
7
|
+
export declare function normalizeDiscordSenderForPermit(input: {
|
|
8
|
+
isOwner?: boolean;
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
}): {
|
|
11
|
+
role?: string;
|
|
12
|
+
permissions?: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function checkDiscordPlatformPermit(perm: string, subject: PermissionSubject): boolean;
|