@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +51 -76
  3. package/adapters/discord.ts +46 -0
  4. package/agent/tools/add_role.ts +24 -0
  5. package/agent/tools/create_thread.ts +25 -0
  6. package/agent/tools/forum_post.ts +24 -0
  7. package/agent/tools/list_roles.ts +22 -0
  8. package/agent/tools/react.ts +22 -0
  9. package/agent/tools/remove_role.ts +24 -0
  10. package/agent/tools/send_embed.ts +37 -0
  11. package/lib/discord-agent-deps.d.ts +35 -0
  12. package/lib/discord-agent-deps.js +32 -0
  13. package/lib/endpoint.d.ts +66 -119
  14. package/lib/endpoint.js +308 -1047
  15. package/lib/gateway.d.ts +122 -0
  16. package/lib/gateway.js +235 -0
  17. package/lib/index.d.ts +6 -18
  18. package/lib/index.js +6 -330
  19. package/lib/platform-permit.d.ts +1 -2
  20. package/lib/platform-permit.js +4 -2
  21. package/lib/protocol.d.ts +135 -0
  22. package/lib/protocol.js +234 -0
  23. package/lib/webhook.d.ts +13 -0
  24. package/lib/webhook.js +86 -0
  25. package/package.json +48 -31
  26. package/plugin.ts +13 -0
  27. package/schema.json +63 -0
  28. package/src/discord-agent-deps.ts +79 -0
  29. package/src/endpoint.ts +385 -1167
  30. package/src/gateway.ts +337 -0
  31. package/src/index.ts +55 -332
  32. package/src/platform-permit.ts +1 -2
  33. package/src/protocol.ts +392 -0
  34. package/src/webhook.ts +121 -0
  35. package/client/Dashboard.tsx +0 -195
  36. package/client/index.tsx +0 -11
  37. package/client/tsconfig.json +0 -7
  38. package/client/utils/api.ts +0 -30
  39. package/dist/index.js +0 -29
  40. package/lib/adapter.d.ts +0 -25
  41. package/lib/adapter.d.ts.map +0 -1
  42. package/lib/adapter.js +0 -96
  43. package/lib/adapter.js.map +0 -1
  44. package/lib/endpoint-interactions.d.ts +0 -34
  45. package/lib/endpoint-interactions.d.ts.map +0 -1
  46. package/lib/endpoint-interactions.js +0 -284
  47. package/lib/endpoint-interactions.js.map +0 -1
  48. package/lib/endpoint.d.ts.map +0 -1
  49. package/lib/endpoint.js.map +0 -1
  50. package/lib/index.d.ts.map +0 -1
  51. package/lib/index.js.map +0 -1
  52. package/lib/platform-permit.d.ts.map +0 -1
  53. package/lib/platform-permit.js.map +0 -1
  54. package/lib/segment-mapper.d.ts +0 -2
  55. package/lib/segment-mapper.d.ts.map +0 -1
  56. package/lib/segment-mapper.js +0 -2
  57. package/lib/segment-mapper.js.map +0 -1
  58. package/lib/types.d.ts +0 -49
  59. package/lib/types.d.ts.map +0 -1
  60. package/lib/types.js +0 -2
  61. package/lib/types.js.map +0 -1
  62. package/plugin.yml +0 -3
  63. package/src/adapter.ts +0 -108
  64. package/src/endpoint-interactions.ts +0 -354
  65. package/src/segment-mapper.ts +0 -1
  66. package/src/types.ts +0 -60
  67. /package/{skills/discord → agent}/PERMITS.md +0 -0
  68. /package/{skills/discord/SKILL.md → agent/skills/discord.md} +0 -0
@@ -0,0 +1,122 @@
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
+ react(emoji: string): Promise<unknown>;
30
+ reactions: {
31
+ resolve(emoji: unknown): {
32
+ users: {
33
+ remove(userId: string): Promise<unknown>;
34
+ };
35
+ } | null;
36
+ cache: {
37
+ find(fn: (r: {
38
+ emoji: {
39
+ toString(): string;
40
+ name?: string | null;
41
+ id?: string | null;
42
+ };
43
+ }) => boolean): {
44
+ users: {
45
+ remove(userId: string): Promise<unknown>;
46
+ };
47
+ } | undefined;
48
+ };
49
+ };
50
+ }>;
51
+ };
52
+ threads?: {
53
+ create(options: Record<string, unknown>): Promise<{
54
+ id: string;
55
+ }>;
56
+ };
57
+ availableTags?: Array<{
58
+ id: string;
59
+ name: string;
60
+ }>;
61
+ } | null>;
62
+ };
63
+ guilds: {
64
+ fetch(id: string): Promise<{
65
+ id: string;
66
+ name: string;
67
+ ownerId: string;
68
+ memberCount: number;
69
+ createdAt?: Date | null;
70
+ iconURL?(options?: {
71
+ size?: number;
72
+ }): string | null;
73
+ roles: {
74
+ fetch(): Promise<unknown>;
75
+ cache: Map<string, {
76
+ id: string;
77
+ name: string;
78
+ hexColor: string;
79
+ position: number;
80
+ permissions: {
81
+ bitfield: bigint;
82
+ };
83
+ }> | {
84
+ map(fn: (role: {
85
+ id: string;
86
+ name: string;
87
+ hexColor: string;
88
+ position: number;
89
+ permissions: {
90
+ bitfield: bigint;
91
+ };
92
+ }) => unknown): unknown[];
93
+ };
94
+ };
95
+ members: {
96
+ fetch(userId: string | {
97
+ limit?: number;
98
+ }): Promise<unknown>;
99
+ ban(userId: string, options?: {
100
+ reason?: string;
101
+ deleteMessageSeconds?: number;
102
+ }): Promise<unknown>;
103
+ unban(userId: string, reason?: string): Promise<unknown>;
104
+ };
105
+ }>;
106
+ cache: {
107
+ values(): IterableIterator<{
108
+ id: string;
109
+ }>;
110
+ };
111
+ };
112
+ }
113
+ export type CreateDiscordClient = (intents: readonly number[]) => DiscordClientTransport;
114
+ export declare function defaultCreateClient(intents: readonly number[]): DiscordClientTransport;
115
+ export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
116
+ export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
117
+ export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;
118
+ export interface DiscordGatewayConnectHandlers {
119
+ onMessage(msg: DiscordInboundMessage): void;
120
+ onButton(interaction: DiscordButtonInbound): void;
121
+ }
122
+ export declare function connectDiscordGatewayClient(client: DiscordClientTransport, config: ResolvedDiscordGatewayConfig, handlers: DiscordGatewayConnectHandlers): Promise<void>;
package/lib/gateway.js ADDED
@@ -0,0 +1,235 @@
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.file && await fileExists(file.file)) {
115
+ files.push(new AttachmentBuilder(createReadStream(file.file), {
116
+ name: file.name || path.basename(file.file),
117
+ }));
118
+ }
119
+ else if (file.url) {
120
+ files.push(new AttachmentBuilder(file.url, { name: file.name || 'attachment' }));
121
+ }
122
+ }
123
+ if (files.length)
124
+ options.files = files;
125
+ }
126
+ if (body.components?.length) {
127
+ options.components = body.components.map((row) => new ActionRowBuilder().addComponents(...row.components.map((btn) => {
128
+ const b = new ButtonBuilder()
129
+ .setCustomId(btn.custom_id)
130
+ .setLabel(btn.label)
131
+ .setDisabled(!!btn.disabled);
132
+ if (btn.style === 4)
133
+ b.setStyle(ButtonStyle.Danger);
134
+ else if (btn.style === 1)
135
+ b.setStyle(ButtonStyle.Primary);
136
+ else
137
+ b.setStyle(ButtonStyle.Secondary);
138
+ return b;
139
+ })));
140
+ }
141
+ return options;
142
+ }
143
+ async function registerSlashCommands(config, applicationId) {
144
+ if (!config.slashCommands?.length)
145
+ return;
146
+ const rest = new REST({ version: '10' }).setToken(config.token);
147
+ if (config.globalCommands) {
148
+ await rest.put(Routes.applicationCommands(applicationId), {
149
+ body: config.slashCommands,
150
+ });
151
+ logger.info(formatCompact({ op: 'slash_commands', scope: 'global' }));
152
+ }
153
+ }
154
+ async function fileExists(filePath) {
155
+ try {
156
+ await fs.access(filePath);
157
+ return true;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ export async function connectDiscordGatewayClient(client, config, handlers) {
164
+ return new Promise((resolve, reject) => {
165
+ let settled = false;
166
+ client.on('messageCreate', (raw) => {
167
+ const msg = normalizeDiscordMessage(raw);
168
+ if (!msg)
169
+ return;
170
+ // clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
171
+ const botId = client.user?.id;
172
+ const mentions = raw.mentions;
173
+ const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
174
+ handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
175
+ });
176
+ client.on('interactionCreate', (raw) => {
177
+ const interaction = raw;
178
+ if (!interaction.isButton?.())
179
+ return;
180
+ void interaction.deferUpdate?.().catch(() => { });
181
+ if (!interaction.channel)
182
+ return;
183
+ handlers.onButton({
184
+ id: interaction.id,
185
+ customId: interaction.customId,
186
+ channelId: interaction.channel.id,
187
+ channelKind: resolveChannelKind(interaction.channel.type),
188
+ userId: interaction.user.id,
189
+ userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
190
+ sourceMessageId: interaction.message?.id,
191
+ });
192
+ });
193
+ client.once('clientReady', () => {
194
+ void (async () => {
195
+ try {
196
+ if (config.defaultActivity && client.user?.setActivity) {
197
+ client.user.setActivity(config.defaultActivity.name, {
198
+ type: activityTypeCode(config.defaultActivity.type),
199
+ url: config.defaultActivity.url,
200
+ });
201
+ }
202
+ if (config.enableSlashCommands && config.slashCommands?.length && client.user) {
203
+ await registerSlashCommands(config, client.user.id);
204
+ }
205
+ if (!settled) {
206
+ settled = true;
207
+ resolve();
208
+ }
209
+ }
210
+ catch (error) {
211
+ if (!settled) {
212
+ settled = true;
213
+ reject(error);
214
+ }
215
+ }
216
+ })();
217
+ });
218
+ client.on('error', (error) => {
219
+ logger.error('Discord client error:', error);
220
+ if (!settled) {
221
+ settled = true;
222
+ reject(error instanceof Error ? error : new Error(String(error)));
223
+ }
224
+ });
225
+ client.on('warn', (info) => {
226
+ logger.warn('Discord client warning:', info);
227
+ });
228
+ client.login(config.token).catch((error) => {
229
+ if (!settled) {
230
+ settled = true;
231
+ reject(error instanceof Error ? error : new Error(String(error)));
232
+ }
233
+ });
234
+ });
235
+ }
package/lib/index.d.ts CHANGED
@@ -1,18 +1,6 @@
1
- import { PageManager } from "@zhin.js/host-api";
2
- import { DiscordAdapter } from "./adapter.js";
3
- declare module "zhin.js" {
4
- namespace Plugin {
5
- interface Contexts {
6
- router: import("@zhin.js/host-router").Router;
7
- web: PageManager;
8
- }
9
- }
10
- interface Adapters {
11
- discord: DiscordAdapter;
12
- }
13
- }
14
- export * from "./types.js";
15
- export { DiscordEndpoint } from "./endpoint.js";
16
- export { DiscordInteractionsEndpoint } from "./endpoint-interactions.js";
17
- export { DiscordAdapter, type DiscordEndpointLike } 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 { getDiscordAgentDeps, registerDiscordAgentEndpoint, setDiscordAgentDeps, type DiscordAgentDeps, type DiscordAgentEndpoint, } from './discord-agent-deps.js';
3
+ export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, registerDiscordPlatformPermitChecker, } 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';