@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.
- package/CHANGELOG.md +169 -0
- package/LICENSE +21 -0
- package/PERMITS.md +15 -14
- package/README.md +6 -2
- package/adapters/discord/index.js +3 -0
- package/adapters/discord/index.ts +3 -0
- package/agents/discord/conversation.js +70 -0
- package/agents/discord/conversation.ts +77 -0
- package/agents/discord/messages.js +18 -2
- package/agents/discord/messages.ts +18 -2
- package/agents/discord/skills/discord/SKILL.md +1 -1
- package/agents/discord/skills/discord/tools/react/index.js +7 -6
- package/agents/discord/skills/discord/tools/react/index.ts +8 -7
- package/agents/discord/skills/discord/tools/send_embed/index.js +6 -11
- package/agents/discord/skills/discord/tools/send_embed/index.ts +7 -12
- package/agents/discord/skills/discord-guild/tools/add_role/index.js +11 -9
- package/agents/discord/skills/discord-guild/tools/add_role/index.ts +11 -10
- package/agents/discord/skills/discord-guild/tools/create_thread/index.js +5 -5
- package/agents/discord/skills/discord-guild/tools/create_thread/index.ts +6 -6
- package/agents/discord/skills/discord-guild/tools/forum_post/index.js +7 -3
- package/agents/discord/skills/discord-guild/tools/forum_post/index.ts +7 -4
- package/agents/discord/skills/discord-guild/tools/list_roles/index.js +7 -8
- package/agents/discord/skills/discord-guild/tools/list_roles/index.ts +8 -9
- package/agents/discord/skills/discord-guild/tools/remove_role/index.js +11 -9
- package/agents/discord/skills/discord-guild/tools/remove_role/index.ts +11 -10
- package/lib/endpoint.js +71 -12
- package/lib/gateway.d.ts +1 -0
- package/lib/gateway.js +21 -8
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/platform-permit.d.ts +4 -11
- package/lib/platform-permit.js +7 -40
- package/lib/protocol.d.ts +18 -0
- package/lib/protocol.js +32 -3
- package/lib/sender-standing.d.ts +18 -0
- package/lib/sender-standing.js +91 -0
- package/lib/webhook.d.ts +2 -6
- package/lib/webhook.js +23 -55
- package/package.json +23 -25
- package/plugin.js +0 -6
- package/schema.json +0 -56
- package/src/endpoint.ts +73 -11
- package/src/gateway.ts +22 -5
- package/src/index.ts +1 -6
- package/src/platform-permit.ts +5 -52
- package/src/protocol.ts +43 -3
- package/src/sender-standing.ts +91 -0
- 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: '
|
|
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({
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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<{
|
|
7
|
-
description: '
|
|
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({
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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: '
|
|
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('
|
|
11
|
+
role_id: z.string().describe('身份组 ID'),
|
|
13
12
|
}),
|
|
14
13
|
adapter: 'discord',
|
|
15
14
|
scopes: ["channel"],
|
|
16
15
|
tags: ['discord'],
|
|
17
|
-
permissions: [
|
|
18
|
-
async execute({
|
|
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(
|
|
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
|
-
|
|
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<{
|
|
8
|
-
description: '
|
|
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('
|
|
11
|
+
role_id: z.string().describe('身份组 ID'),
|
|
13
12
|
}),
|
|
14
13
|
adapter: 'discord',
|
|
15
14
|
scopes: ["channel"],
|
|
16
15
|
tags: ['discord'],
|
|
17
|
-
permissions: [
|
|
18
|
-
async execute({
|
|
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(
|
|
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
|
-
|
|
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: '
|
|
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: [
|
|
20
|
-
async execute({
|
|
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<{
|
|
9
|
-
description: '
|
|
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: [
|
|
20
|
-
async execute({
|
|
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: '
|
|
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({
|
|
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<{
|
|
8
|
-
description: '
|
|
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({
|
|
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: '
|
|
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: [
|
|
15
|
-
async execute(
|
|
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(
|
|
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<
|
|
7
|
-
description: '
|
|
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: [
|
|
15
|
-
async execute(
|
|
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(
|
|
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: '
|
|
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('
|
|
11
|
+
role_id: z.string().describe('身份组 ID'),
|
|
13
12
|
}),
|
|
14
13
|
adapter: 'discord',
|
|
15
14
|
scopes: ["channel"],
|
|
16
15
|
tags: ['discord'],
|
|
17
|
-
permissions: [
|
|
18
|
-
async execute({
|
|
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(
|
|
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
|
-
|
|
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<{
|
|
8
|
-
description: '
|
|
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('
|
|
11
|
+
role_id: z.string().describe('身份组 ID'),
|
|
13
12
|
}),
|
|
14
13
|
adapter: 'discord',
|
|
15
14
|
scopes: ["channel"],
|
|
16
15
|
tags: ['discord'],
|
|
17
|
-
permissions: [
|
|
18
|
-
async execute({
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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
|
|
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>;
|