@zhin.js/adapter-discord 1.1.27 → 1.1.28

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 (48) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/LICENSE +21 -0
  3. package/PERMITS.md +15 -14
  4. package/README.md +6 -2
  5. package/adapters/discord/index.js +3 -0
  6. package/adapters/discord/index.ts +3 -0
  7. package/agents/discord/conversation.js +70 -0
  8. package/agents/discord/conversation.ts +77 -0
  9. package/agents/discord/messages.js +18 -2
  10. package/agents/discord/messages.ts +18 -2
  11. package/agents/discord/skills/discord/SKILL.md +1 -1
  12. package/agents/discord/skills/discord/tools/react/index.js +7 -6
  13. package/agents/discord/skills/discord/tools/react/index.ts +8 -7
  14. package/agents/discord/skills/discord/tools/send_embed/index.js +6 -11
  15. package/agents/discord/skills/discord/tools/send_embed/index.ts +7 -12
  16. package/agents/discord/skills/discord-guild/tools/add_role/index.js +11 -9
  17. package/agents/discord/skills/discord-guild/tools/add_role/index.ts +11 -10
  18. package/agents/discord/skills/discord-guild/tools/create_thread/index.js +5 -5
  19. package/agents/discord/skills/discord-guild/tools/create_thread/index.ts +6 -6
  20. package/agents/discord/skills/discord-guild/tools/forum_post/index.js +7 -3
  21. package/agents/discord/skills/discord-guild/tools/forum_post/index.ts +7 -4
  22. package/agents/discord/skills/discord-guild/tools/list_roles/index.js +7 -8
  23. package/agents/discord/skills/discord-guild/tools/list_roles/index.ts +8 -9
  24. package/agents/discord/skills/discord-guild/tools/remove_role/index.js +11 -9
  25. package/agents/discord/skills/discord-guild/tools/remove_role/index.ts +11 -10
  26. package/lib/endpoint.js +71 -12
  27. package/lib/gateway.d.ts +1 -0
  28. package/lib/gateway.js +21 -8
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/platform-permit.d.ts +4 -11
  32. package/lib/platform-permit.js +7 -40
  33. package/lib/protocol.d.ts +18 -0
  34. package/lib/protocol.js +32 -3
  35. package/lib/sender-standing.d.ts +18 -0
  36. package/lib/sender-standing.js +91 -0
  37. package/lib/webhook.d.ts +2 -6
  38. package/lib/webhook.js +23 -55
  39. package/package.json +23 -25
  40. package/plugin.js +0 -6
  41. package/schema.json +0 -56
  42. package/src/endpoint.ts +73 -11
  43. package/src/gateway.ts +22 -5
  44. package/src/index.ts +1 -6
  45. package/src/platform-permit.ts +5 -52
  46. package/src/protocol.ts +43 -3
  47. package/src/sender-standing.ts +91 -0
  48. package/src/webhook.ts +21 -58
@@ -1,12 +1,11 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { sendToCurrentConversation } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
- import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
6
6
  export default defineAgentTool({
7
- description: '发送 Discord 富文本嵌入消息(Embed)',
7
+ description: '在当前 Discord 频道(或线程)发送富文本嵌入消息(Embed)',
8
8
  inputSchema: z.object({
9
- channel_id: z.string().describe('频道 ID'),
10
9
  title: z.string().optional().describe('Embed 标题'),
11
10
  description: z.string().optional().describe('Embed 描述'),
12
11
  color: z.number().optional().describe('颜色值(十进制,如 0x00ff00 = 65280)'),
@@ -15,8 +14,7 @@ export default defineAgentTool({
15
14
  }),
16
15
  adapter: 'discord',
17
16
  tags: ['discord'],
18
- async execute({ channel_id, title, description, color, url, fields }, context) {
19
- const client = requireDiscordGatewayClient(context.$client);
17
+ async execute({ title, description, color, url, fields }, context) {
20
18
  const embedData = {};
21
19
  if (title)
22
20
  embedData.title = title;
@@ -34,11 +32,8 @@ export default defineAgentTool({
34
32
  return { success: false, message: t(context.locale, 'invalidFields') };
35
33
  }
36
34
  }
37
- const channel = await client.channels.fetch(channel_id);
38
- if (!channel?.isSendable()) {
39
- throw new Error(t(context.locale, 'notTextChannel', { p0: channel_id }));
40
- }
41
- const msg = await channel.send({ embeds: [embedData] });
42
- return { success: true, message_id: msg.id, message: t(context.locale, 'embedSent') };
35
+ // `discord:embed` is the Discord adapter's namespaced outbound segment (rendered by formatOutboundBody).
36
+ const result = await sendToCurrentConversation(context, { type: 'discord:embed', data: embedData });
37
+ return { ...result, message: t(context.locale, result.success ? 'embedSent' : 'operationFailed') };
43
38
  },
44
39
  });
@@ -1,12 +1,11 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { sendToCurrentConversation } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
- import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
5
 
6
- export default defineAgentTool<{ channel_id: string; title?: string; description?: string; color?: number; url?: string; fields?: string }>({
7
- description: '发送 Discord 富文本嵌入消息(Embed)',
6
+ export default defineAgentTool<{ title?: string; description?: string; color?: number; url?: string; fields?: string }>({
7
+ description: '在当前 Discord 频道(或线程)发送富文本嵌入消息(Embed)',
8
8
  inputSchema: z.object({
9
- channel_id: z.string().describe('频道 ID'),
10
9
  title: z.string().optional().describe('Embed 标题'),
11
10
  description: z.string().optional().describe('Embed 描述'),
12
11
  color: z.number().optional().describe('颜色值(十进制,如 0x00ff00 = 65280)'),
@@ -15,8 +14,7 @@ export default defineAgentTool<{ channel_id: string; title?: string; description
15
14
  }),
16
15
  adapter: 'discord',
17
16
  tags: ['discord'],
18
- async execute({ channel_id, title, description, color, url, fields }: { channel_id: string; title?: string; description?: string; color?: number; url?: string; fields?: string }, context) {
19
- const client = requireDiscordGatewayClient(context.$client);
17
+ async execute({ title, description, color, url, fields }, context) {
20
18
  const embedData: Record<string, unknown> = {};
21
19
  if (title) embedData.title = title;
22
20
  if (description) embedData.description = description;
@@ -29,11 +27,8 @@ export default defineAgentTool<{ channel_id: string; title?: string; description
29
27
  return { success: false, message: t(context.locale, 'invalidFields') };
30
28
  }
31
29
  }
32
- const channel = await client.channels.fetch(channel_id);
33
- if (!channel?.isSendable()) {
34
- throw new Error(t(context.locale, 'notTextChannel', { p0: channel_id }));
35
- }
36
- const msg = await channel.send({ embeds: [embedData] } as never);
37
- return { success: true, message_id: msg.id, message: t(context.locale, 'embedSent') };
30
+ // `discord:embed` is the Discord adapter's namespaced outbound segment (rendered by formatOutboundBody).
31
+ const result = await sendToCurrentConversation(context, { type: 'discord:embed', data: embedData });
32
+ return { ...result, message: t(context.locale, result.success ? 'embedSent' : 'operationFailed') };
38
33
  },
39
34
  });
@@ -1,26 +1,28 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentGuildChannel, requireRoleBelowRequester } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
6
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
6
- import { platformPermission } from '@zhin.js/permission';
7
7
  export default defineAgentTool({
8
- description: '给成员添加 Discord 角色',
8
+ description: '给当前 Discord 服务器的成员添加身份组(只能是低于你最高身份组的身份组)',
9
9
  inputSchema: z.object({
10
- guild_id: z.string().describe('服务器 ID'),
11
10
  user_id: z.string().describe('用户 ID'),
12
- role_id: z.string().describe('角色 ID'),
11
+ role_id: z.string().describe('身份组 ID'),
13
12
  }),
14
13
  adapter: 'discord',
15
14
  scopes: ["channel"],
16
15
  tags: ['discord'],
17
- permissions: [platformPermission('discord', 'manage_roles')],
18
- async execute({ guild_id, user_id, role_id }, context) {
16
+ permissions: ['platform-permission:manage_roles', 'bot-permission:manage_roles'],
17
+ async execute({ user_id, role_id }, context) {
18
+ const { guildId } = currentGuildChannel(context);
19
19
  const client = requireDiscordGatewayClient(context.$client);
20
- const guild = await client.guilds.fetch(guild_id);
20
+ const guild = await client.guilds.fetch(guildId);
21
+ const denied = await requireRoleBelowRequester(context, guild, role_id);
22
+ if (denied)
23
+ return { success: false, message: denied };
21
24
  const member = await guild.members.fetch(user_id);
22
25
  await member.roles.add(role_id);
23
- const success = true;
24
- return { success, message: success ? t(context.locale, 'memberRoleAdded', { p0: user_id }) : t(context.locale, 'operationFailed') };
26
+ return { success: true, message: t(context.locale, 'memberRoleAdded', { p0: user_id }) };
25
27
  },
26
28
  });
@@ -1,28 +1,29 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentGuildChannel, requireRoleBelowRequester } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
- import { platformPermission } from '@zhin.js/permission';
6
6
 
7
- export default defineAgentTool<{ guild_id: string; user_id: string; role_id: string }>({
8
- description: '给成员添加 Discord 角色',
7
+ export default defineAgentTool<{ user_id: string; role_id: string }>({
8
+ description: '给当前 Discord 服务器的成员添加身份组(只能是低于你最高身份组的身份组)',
9
9
  inputSchema: z.object({
10
- guild_id: z.string().describe('服务器 ID'),
11
10
  user_id: z.string().describe('用户 ID'),
12
- role_id: z.string().describe('角色 ID'),
11
+ role_id: z.string().describe('身份组 ID'),
13
12
  }),
14
13
  adapter: 'discord',
15
14
  scopes: ["channel"],
16
15
  tags: ['discord'],
17
- permissions: [platformPermission('discord', 'manage_roles')],
18
- async execute({ guild_id, user_id, role_id }: { guild_id: string; user_id: string; role_id: string }, context) {
16
+ permissions: ['platform-permission:manage_roles', 'bot-permission:manage_roles'],
17
+ async execute({ user_id, role_id }, context) {
18
+ const { guildId } = currentGuildChannel(context);
19
19
  const client = requireDiscordGatewayClient(context.$client);
20
- const guild = await client.guilds.fetch(guild_id);
20
+ const guild = await client.guilds.fetch(guildId);
21
+ const denied = await requireRoleBelowRequester(context, guild, role_id);
22
+ if (denied) return { success: false, message: denied };
21
23
  const member = await guild.members.fetch(user_id) as {
22
24
  roles: { add(id: string): Promise<unknown> };
23
25
  };
24
26
  await member.roles.add(role_id);
25
- const success = true;
26
- return { success, message: success ? t(context.locale, 'memberRoleAdded', { p0: user_id }) : t(context.locale, 'operationFailed') };
27
+ return { success: true, message: t(context.locale, 'memberRoleAdded', { p0: user_id }) };
27
28
  },
28
29
  });
@@ -1,14 +1,13 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentGuildChannel } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
6
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
6
- import { platformPermission } from '@zhin.js/permission';
7
7
  import { ThreadAutoArchiveDuration } from 'discord.js';
8
8
  export default defineAgentTool({
9
- description: '在 Discord 频道中创建帖子/子线程',
9
+ description: '在当前 Discord 频道中创建帖子/子线程',
10
10
  inputSchema: z.object({
11
- channel_id: z.string().describe('频道 ID'),
12
11
  name: z.string().describe('帖子标题'),
13
12
  message_id: z.string().optional().describe('基于某条消息创建(可选)'),
14
13
  auto_archive_duration: z.number().optional().describe('自动归档时间(分钟:60/1440/4320/10080)'),
@@ -16,8 +15,9 @@ export default defineAgentTool({
16
15
  adapter: 'discord',
17
16
  scopes: ["channel"],
18
17
  tags: ['discord'],
19
- permissions: [platformPermission('discord', 'manage_channels')],
20
- async execute({ channel_id, name, message_id, auto_archive_duration }, context) {
18
+ permissions: ['platform-permission:manage_channels', 'bot-permission:create_public_threads'],
19
+ async execute({ name, message_id, auto_archive_duration }, context) {
20
+ const { channelId: channel_id } = currentGuildChannel(context);
21
21
  const client = requireDiscordGatewayClient(context.$client);
22
22
  const channel = await client.channels.fetch(channel_id);
23
23
  if (!channel || !('threads' in channel) || channel.isThreadOnly())
@@ -1,14 +1,13 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentGuildChannel } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
- import { platformPermission } from '@zhin.js/permission';
6
6
  import { ThreadAutoArchiveDuration } from 'discord.js';
7
7
 
8
- export default defineAgentTool<{ channel_id: string; name: string; message_id?: string; auto_archive_duration?: number }>({
9
- description: '在 Discord 频道中创建帖子/子线程',
8
+ export default defineAgentTool<{ name: string; message_id?: string; auto_archive_duration?: number }>({
9
+ description: '在当前 Discord 频道中创建帖子/子线程',
10
10
  inputSchema: z.object({
11
- channel_id: z.string().describe('频道 ID'),
12
11
  name: z.string().describe('帖子标题'),
13
12
  message_id: z.string().optional().describe('基于某条消息创建(可选)'),
14
13
  auto_archive_duration: z.number().optional().describe('自动归档时间(分钟:60/1440/4320/10080)'),
@@ -16,8 +15,9 @@ export default defineAgentTool<{ channel_id: string; name: string; message_id?:
16
15
  adapter: 'discord',
17
16
  scopes: ["channel"],
18
17
  tags: ['discord'],
19
- permissions: [platformPermission('discord', 'manage_channels')],
20
- async execute({ channel_id, name, message_id, auto_archive_duration }: { channel_id: string; name: string; message_id?: string; auto_archive_duration?: number }, context) {
18
+ permissions: ['platform-permission:manage_channels', 'bot-permission:create_public_threads'],
19
+ async execute({ name, message_id, auto_archive_duration }, context) {
20
+ const { channelId: channel_id } = currentGuildChannel(context);
21
21
  const client = requireDiscordGatewayClient(context.$client);
22
22
  const channel = await client.channels.fetch(channel_id);
23
23
  if (!channel || !('threads' in channel) || channel.isThreadOnly()) throw new Error(t(context.locale, 'threadsUnsupported', { p0: channel_id }));
@@ -1,13 +1,13 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentChannel } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
6
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
6
7
  import { ChannelType } from 'discord.js';
7
8
  export default defineAgentTool({
8
- description: '在 Discord 论坛频道中创建帖子',
9
+ description: '在当前 Discord 论坛频道中创建帖子(需在该论坛的帖子里发起)',
9
10
  inputSchema: z.object({
10
- channel_id: z.string().describe('论坛频道 ID'),
11
11
  name: z.string().describe('帖子标题'),
12
12
  content: z.string().describe('帖子内容'),
13
13
  tags: z.string().optional().describe('标签名,逗号分隔(可选)'),
@@ -15,7 +15,11 @@ export default defineAgentTool({
15
15
  adapter: 'discord',
16
16
  scopes: ["channel"],
17
17
  tags: ['discord'],
18
- async execute({ channel_id, name, content, tags }, context) {
18
+ async execute({ name, content, tags }, context) {
19
+ // A forum post reports the forum as its conversation id and the post as `threadId`.
20
+ const { conversationChannelId: channel_id, guildId } = currentChannel(context);
21
+ if (!guildId)
22
+ throw new Error(t(context.locale, 'guildChannelRequired'));
19
23
  const client = requireDiscordGatewayClient(context.$client);
20
24
  const tagList = tags ? tags.split(',').map((t) => t.trim()) : undefined;
21
25
  const channel = await client.channels.fetch(channel_id);
@@ -1,13 +1,13 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentChannel } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
6
  import { ChannelType } from 'discord.js';
6
7
 
7
- export default defineAgentTool<{ channel_id: string; name: string; content: string; tags?: string }>({
8
- description: '在 Discord 论坛频道中创建帖子',
8
+ export default defineAgentTool<{ name: string; content: string; tags?: string }>({
9
+ description: '在当前 Discord 论坛频道中创建帖子(需在该论坛的帖子里发起)',
9
10
  inputSchema: z.object({
10
- channel_id: z.string().describe('论坛频道 ID'),
11
11
  name: z.string().describe('帖子标题'),
12
12
  content: z.string().describe('帖子内容'),
13
13
  tags: z.string().optional().describe('标签名,逗号分隔(可选)'),
@@ -15,7 +15,10 @@ export default defineAgentTool<{ channel_id: string; name: string; content: stri
15
15
  adapter: 'discord',
16
16
  scopes: ["channel"],
17
17
  tags: ['discord'],
18
- async execute({ channel_id, name, content, tags }: { channel_id: string; name: string; content: string; tags?: string }, context) {
18
+ async execute({ name, content, tags }, context) {
19
+ // A forum post reports the forum as its conversation id and the post as `threadId`.
20
+ const { conversationChannelId: channel_id, guildId } = currentChannel(context);
21
+ if (!guildId) throw new Error(t(context.locale, 'guildChannelRequired'));
19
22
  const client = requireDiscordGatewayClient(context.$client);
20
23
  const tagList = tags ? tags.split(',').map((t: string) => t.trim()) : undefined;
21
24
  const channel = await client.channels.fetch(channel_id);
@@ -1,20 +1,19 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { currentGuildChannel } from '../../../../conversation.js';
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
- import { platformPermission } from '@zhin.js/permission';
6
6
  export default defineAgentTool({
7
- description: '获取 Discord 服务器角色列表',
8
- inputSchema: z.object({
9
- guild_id: z.string().describe('服务器 ID'),
10
- }),
7
+ description: '获取当前 Discord 服务器的身份组列表',
8
+ inputSchema: z.object({}),
11
9
  adapter: 'discord',
12
10
  scopes: ["channel"],
13
11
  tags: ['discord'],
14
- permissions: [platformPermission('discord', 'manage_roles')],
15
- async execute({ guild_id }, context) {
12
+ permissions: ['platform-permission:manage_roles'],
13
+ async execute(_args, context) {
14
+ const { guildId } = currentGuildChannel(context);
16
15
  const client = requireDiscordGatewayClient(context.$client);
17
- const guild = await client.guilds.fetch(guild_id);
16
+ const guild = await client.guilds.fetch(guildId);
18
17
  await guild.roles.fetch();
19
18
  const cache = guild.roles.cache;
20
19
  const roles = [...cache.values()].map((role) => ({
@@ -1,20 +1,19 @@
1
+ import { currentGuildChannel } from '../../../../conversation.js';
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { z } from 'zod';
3
4
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
4
- import { platformPermission } from '@zhin.js/permission';
5
5
 
6
- export default defineAgentTool<{ guild_id: string }>({
7
- description: '获取 Discord 服务器角色列表',
8
- inputSchema: z.object({
9
- guild_id: z.string().describe('服务器 ID'),
10
- }),
6
+ export default defineAgentTool<Record<string, never>>({
7
+ description: '获取当前 Discord 服务器的身份组列表',
8
+ inputSchema: z.object({}),
11
9
  adapter: 'discord',
12
10
  scopes: ["channel"],
13
11
  tags: ['discord'],
14
- permissions: [platformPermission('discord', 'manage_roles')],
15
- async execute({ guild_id }: { guild_id: string }, context) {
12
+ permissions: ['platform-permission:manage_roles'],
13
+ async execute(_args, context) {
14
+ const { guildId } = currentGuildChannel(context);
16
15
  const client = requireDiscordGatewayClient(context.$client);
17
- const guild = await client.guilds.fetch(guild_id);
16
+ const guild = await client.guilds.fetch(guildId);
18
17
  await guild.roles.fetch();
19
18
  const cache = guild.roles.cache as Map<string, {
20
19
  id: string;
@@ -1,26 +1,28 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentGuildChannel, requireRoleBelowRequester } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
6
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
6
- import { platformPermission } from '@zhin.js/permission';
7
7
  export default defineAgentTool({
8
- description: '移除成员的 Discord 角色',
8
+ description: '移除当前 Discord 服务器成员的身份组(只能是低于你最高身份组的身份组)',
9
9
  inputSchema: z.object({
10
- guild_id: z.string().describe('服务器 ID'),
11
10
  user_id: z.string().describe('用户 ID'),
12
- role_id: z.string().describe('角色 ID'),
11
+ role_id: z.string().describe('身份组 ID'),
13
12
  }),
14
13
  adapter: 'discord',
15
14
  scopes: ["channel"],
16
15
  tags: ['discord'],
17
- permissions: [platformPermission('discord', 'manage_roles')],
18
- async execute({ guild_id, user_id, role_id }, context) {
16
+ permissions: ['platform-permission:manage_roles', 'bot-permission:manage_roles'],
17
+ async execute({ user_id, role_id }, context) {
18
+ const { guildId } = currentGuildChannel(context);
19
19
  const client = requireDiscordGatewayClient(context.$client);
20
- const guild = await client.guilds.fetch(guild_id);
20
+ const guild = await client.guilds.fetch(guildId);
21
+ const denied = await requireRoleBelowRequester(context, guild, role_id);
22
+ if (denied)
23
+ return { success: false, message: denied };
21
24
  const member = await guild.members.fetch(user_id);
22
25
  await member.roles.remove(role_id);
23
- const success = true;
24
- return { success, message: success ? t(context.locale, 'memberRoleRemoved', { p0: user_id }) : t(context.locale, 'operationFailed') };
26
+ return { success: true, message: t(context.locale, 'memberRoleRemoved', { p0: user_id }) };
25
27
  },
26
28
  });
@@ -1,28 +1,29 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentGuildChannel, requireRoleBelowRequester } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { requireDiscordGatewayClient } from '@zhin.js/adapter-discord';
5
- import { platformPermission } from '@zhin.js/permission';
6
6
 
7
- export default defineAgentTool<{ guild_id: string; user_id: string; role_id: string }>({
8
- description: '移除成员的 Discord 角色',
7
+ export default defineAgentTool<{ user_id: string; role_id: string }>({
8
+ description: '移除当前 Discord 服务器成员的身份组(只能是低于你最高身份组的身份组)',
9
9
  inputSchema: z.object({
10
- guild_id: z.string().describe('服务器 ID'),
11
10
  user_id: z.string().describe('用户 ID'),
12
- role_id: z.string().describe('角色 ID'),
11
+ role_id: z.string().describe('身份组 ID'),
13
12
  }),
14
13
  adapter: 'discord',
15
14
  scopes: ["channel"],
16
15
  tags: ['discord'],
17
- permissions: [platformPermission('discord', 'manage_roles')],
18
- async execute({ guild_id, user_id, role_id }: { guild_id: string; user_id: string; role_id: string }, context) {
16
+ permissions: ['platform-permission:manage_roles', 'bot-permission:manage_roles'],
17
+ async execute({ user_id, role_id }, context) {
18
+ const { guildId } = currentGuildChannel(context);
19
19
  const client = requireDiscordGatewayClient(context.$client);
20
- const guild = await client.guilds.fetch(guild_id);
20
+ const guild = await client.guilds.fetch(guildId);
21
+ const denied = await requireRoleBelowRequester(context, guild, role_id);
22
+ if (denied) return { success: false, message: denied };
21
23
  const member = await guild.members.fetch(user_id) as {
22
24
  roles: { remove(id: string): Promise<unknown> };
23
25
  };
24
26
  await member.roles.remove(role_id);
25
- const success = true;
26
- return { success, message: success ? t(context.locale, 'memberRoleRemoved', { p0: user_id }) : t(context.locale, 'operationFailed') };
27
+ return { success: true, message: t(context.locale, 'memberRoleRemoved', { p0: user_id }) };
27
28
  },
28
29
  });
package/lib/endpoint.js CHANGED
@@ -7,10 +7,11 @@ import { isDiscordChannelObfuscated } from './channel-visibility.js';
7
7
  import { Client, ChannelType } from 'discord.js';
8
8
  import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
9
9
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
10
- import { connectDiscordGatewayClient, discordClientOptions, DEFAULT_INTENTS, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
11
- import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
10
+ import { connectDiscordGatewayClient, discordClientOptions, DEFAULT_INTENTS, toMessageCreateOptions, } from './gateway.js';
11
+ import { discordChannelOf, discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
12
12
  import { registerDiscordInteractionRoutes } from './webhook.js';
13
13
  import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
14
+ import { discordGuildPermissionNamesFromChannelBitfield, discordPermissionNames, discordSenderStanding, discordStandingFromPermissions, } from './sender-standing.js';
14
15
  const DISCORD_API = 'https://discord.com/api/v10';
15
16
  /** 出站 HTTP 调用统一 30s 超时。 */
16
17
  const OUTBOUND_TIMEOUT_MS = 30_000;
@@ -138,11 +139,11 @@ export class DiscordGatewayEndpoint extends Client {
138
139
  }
139
140
  async #sendFramework({ conversation, payload, signal }) {
140
141
  const body = formatOutboundBody(payload);
141
- const snowflake = await this.#sendBody({ channelId: conversation.id, body, signal });
142
+ const snowflake = await this.#sendBody({ channelId: discordChannelOf(conversation), body, signal });
142
143
  this.#logger.debug(formatCompact({
143
144
  op: 'discord_send',
144
145
  endpoint: this.#options.config.id,
145
- target: conversation.id,
146
+ target: discordChannelOf(conversation),
146
147
  messageId: snowflake,
147
148
  }));
148
149
  return snowflake;
@@ -151,7 +152,7 @@ export class DiscordGatewayEndpoint extends Client {
151
152
  if (!message.id)
152
153
  return;
153
154
  const operation = createEndpointOperation({ signal });
154
- const channel = await operation.prepare(() => this.#requireClient().channels.fetch(message.conversation.id));
155
+ const channel = await operation.prepare(() => this.#requireClient().channels.fetch(discordChannelOf(message.conversation)));
155
156
  if (!channel?.isTextBased() || !channel.messages)
156
157
  return;
157
158
  const msg = await operation.prepare(() => channel.messages.fetch(message.id));
@@ -164,6 +165,7 @@ export class DiscordGatewayEndpoint extends Client {
164
165
  if (msg.authorBot)
165
166
  return;
166
167
  const conversation = discordInboundConversation(String(this.#options.id), msg);
168
+ const platform = discordSenderStanding(msg);
167
169
  void this[endpointSpec].events.emit('message.receive', {
168
170
  conversation,
169
171
  event: msg.event,
@@ -174,7 +176,7 @@ export class DiscordGatewayEndpoint extends Client {
174
176
  sender: {
175
177
  id: msg.authorId,
176
178
  name: senderDisplayName(msg) || undefined,
177
- ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
179
+ ...(platform ? { platform } : {}),
178
180
  },
179
181
  endpointId: this.#options.config.id,
180
182
  ...(msg.mentionedBot ? { mentioned: true } : {}),
@@ -184,7 +186,7 @@ export class DiscordGatewayEndpoint extends Client {
184
186
  userId: msg.authorId,
185
187
  guildId: msg.guildId,
186
188
  permissions: msg.permissionTokens,
187
- role: resolveSenderRole(msg),
189
+ role: platform?.standing,
188
190
  }),
189
191
  }).catch((err) => {
190
192
  this.#logger.warn(formatCompact({
@@ -222,7 +224,7 @@ export class DiscordGatewayEndpoint extends Client {
222
224
  });
223
225
  }
224
226
  async #addReaction({ message, emoji, hint, signal }) {
225
- const channelId = hint?.channelId ?? message.conversation.id;
227
+ const channelId = hint?.channelId ?? discordChannelOf(message.conversation);
226
228
  if (!channelId || !message.id)
227
229
  return null;
228
230
  const operation = createEndpointOperation({ signal });
@@ -326,6 +328,8 @@ export class DiscordRestClient {
326
328
  return this.request({ method: 'DELETE', path: `/channels/${channelId}/messages/${messageId}`, signal });
327
329
  }
328
330
  }
331
+ const APP_PERMISSIONS_TTL_MS = 10 * 60_000;
332
+ const APP_PERMISSIONS_MAX = 1_000;
329
333
  export class DiscordInteractionsEndpoint extends DiscordRestClient {
330
334
  [endpointSpec];
331
335
  #logger;
@@ -334,6 +338,8 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
334
338
  #routeReleases = [];
335
339
  #open = false;
336
340
  #started = false;
341
+ /** Latest interaction `app_permissions` per guild (guild-level names), for the Bot's own standing. */
342
+ #appPermissions = new Map();
337
343
  #control = Object.freeze({
338
344
  recall: request => this.#recallMessage(request),
339
345
  });
@@ -348,6 +354,12 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
348
354
  this[endpointSpec] = createEndpointSpecification(this, {
349
355
  start: () => this.#startFramework(), open: () => this.#openFramework(), close: () => this.#closeFramework(),
350
356
  stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
357
+ // Interactions mode has no gateway session to read the bot's guild member: its own standing
358
+ // comes from the latest interaction's `app_permissions` in that guild (ADR 0056 §1), reduced
359
+ // to guild-level names like senders; without a recent interaction it stays unknown.
360
+ management: Object.freeze({
361
+ inspectSelfStanding: async ({ conversation }) => this.#selfStandingFromInteractions(conversation),
362
+ }),
351
363
  control: this.#control, content: this.#content,
352
364
  profile: createDiscordRestSelfProfile(this, id => this[endpointSpec].reportSelfUserId(id)),
353
365
  });
@@ -385,18 +397,41 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
385
397
  }
386
398
  async #sendFramework({ conversation, payload, signal }) {
387
399
  const body = formatOutboundBody(payload);
388
- const data = await this.createMessage({ channelId: conversation.id, body, signal });
400
+ const data = await this.createMessage({ channelId: discordChannelOf(conversation), body, signal });
389
401
  return data.id ?? '';
390
402
  }
391
403
  async #recallMessage({ message, signal }) {
392
404
  if (!message.id)
393
405
  return;
394
- await this.deleteMessage({ channelId: message.conversation.id, messageId: message.id, signal });
406
+ await this.deleteMessage({ channelId: discordChannelOf(message.conversation), messageId: message.id, signal });
407
+ }
408
+ #selfStandingFromInteractions(conversation) {
409
+ const guildId = conversation.kind === 'channel' && conversation.parent?.kind === 'channel' ? conversation.parent.id : undefined;
410
+ const entry = guildId ? this.#appPermissions.get(guildId) : undefined;
411
+ if (!entry || Date.now() - entry.at > APP_PERMISSIONS_TTL_MS)
412
+ return undefined;
413
+ // The guild owner is not visible to an interaction; `administrator` still reads as `admin`.
414
+ return { standing: discordStandingFromPermissions(entry.names), ...(entry.names.length ? { permissions: entry.names } : {}) };
415
+ }
416
+ #recordAppPermissions(msg) {
417
+ const appPermissions = msg.event?.app_permissions;
418
+ if (!msg.guildId || appPermissions === undefined)
419
+ return;
420
+ this.#appPermissions.delete(msg.guildId);
421
+ this.#appPermissions.set(msg.guildId, { names: discordGuildPermissionNamesFromChannelBitfield(appPermissions), at: Date.now() });
422
+ while (this.#appPermissions.size > APP_PERMISSIONS_MAX) {
423
+ const oldest = this.#appPermissions.keys().next().value;
424
+ if (oldest === undefined)
425
+ break;
426
+ this.#appPermissions.delete(oldest);
427
+ }
395
428
  }
396
429
  admit(msg) {
397
430
  if (!this.#open)
398
431
  return;
432
+ this.#recordAppPermissions(msg);
399
433
  const conversation = discordInboundConversation(String(this.#options.id), msg);
434
+ const platform = discordSenderStanding(msg);
400
435
  void this[endpointSpec].events.emit('message.receive', {
401
436
  conversation,
402
437
  event: msg.event,
@@ -407,7 +442,7 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
407
442
  sender: {
408
443
  id: msg.authorId,
409
444
  name: senderDisplayName(msg) || undefined,
410
- ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
445
+ ...(platform ? { platform } : {}),
411
446
  },
412
447
  endpointId: this.#options.config.id,
413
448
  metadata: Object.freeze({
@@ -475,7 +510,7 @@ async function resolveDiscordContent(fetch, token, reference, context) {
475
510
  }
476
511
  try {
477
512
  context.signal.throwIfAborted();
478
- const response = await fetch(`${DISCORD_API}/channels/${reference.message.conversation.id}/messages/${reference.message.id}`, { headers: { Authorization: `Bot ${token}` }, signal: context.signal });
513
+ const response = await fetch(`${DISCORD_API}/channels/${discordChannelOf(reference.message.conversation)}/messages/${reference.message.id}`, { headers: { Authorization: `Bot ${token}` }, signal: context.signal });
479
514
  if (response.status === 404)
480
515
  return Object.freeze({ status: 'not_found', code: 'discord_message_not_found' });
481
516
  if (response.status === 403)
@@ -520,6 +555,30 @@ async function resolveDiscordContent(fetch, token, reference, context) {
520
555
  */
521
556
  export function createDiscordEndpointManagement(requireClient) {
522
557
  return Object.freeze({
558
+ /**
559
+ * ADR 0056 §1: the Bot's own guild member in the conversation's guild — guild owner → `owner`;
560
+ * `administrator` / `moderate_members` → `admin`; roles and **guild-level** permission names
561
+ * (`GuildMember.permissions`, as for senders) so `bot-permission:*` and `platform-permission:*`
562
+ * read the same scope. Channel overwrites are left to the platform at execution time.
563
+ */
564
+ async inspectSelfStanding({ conversation, signal }) {
565
+ if (conversation.kind === 'private')
566
+ return undefined;
567
+ const operation = createEndpointOperation({ signal });
568
+ const client = requireClient();
569
+ const channel = await operation.run(() => client.channels.fetch(conversation.threadId ?? conversation.id));
570
+ if (!channel || !('guild' in channel) || !client.user)
571
+ return undefined;
572
+ const guild = channel.guild;
573
+ const me = guild.members.me ?? await operation.run(() => guild.members.fetchMe());
574
+ const permissions = me.permissions;
575
+ const names = permissions ? discordPermissionNames(bit => permissions.has(bit)) : [];
576
+ const roles = [...me.roles.cache.values()]
577
+ .filter(role => role.id !== guild.id)
578
+ .map(role => (role.name ? { id: String(role.id), name: String(role.name) } : { id: String(role.id) }));
579
+ const standing = discordStandingFromPermissions(names, guild.ownerId === client.user.id);
580
+ return { standing, ...(roles.length ? { roles } : {}), ...(names.length ? { permissions: names } : {}) };
581
+ },
523
582
  async listGroups(request = {}) {
524
583
  return createEndpointOperation(request).run(() => {
525
584
  const groups = [];
package/lib/gateway.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare const DEFAULT_INTENTS: GatewayIntentBits[];
5
5
  /** Native SDK surface; tests inject transport below the Client rather than replacing its identity. */
6
6
  export type DiscordClientTransport = Client;
7
7
  export declare function discordClientOptions(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): ClientOptions;
8
+ /** Normalized standing word (`owner` / `admin` / `member`); `undefined` outside a guild. */
8
9
  export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
9
10
  export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
10
11
  export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;