@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.
- package/CHANGELOG.md +55 -0
- package/README.md +51 -76
- package/adapters/discord.ts +46 -0
- package/agent/tools/add_role.ts +24 -0
- package/agent/tools/create_thread.ts +25 -0
- package/agent/tools/forum_post.ts +24 -0
- package/agent/tools/list_roles.ts +22 -0
- package/agent/tools/react.ts +22 -0
- package/agent/tools/remove_role.ts +24 -0
- package/agent/tools/send_embed.ts +37 -0
- package/lib/discord-agent-deps.d.ts +35 -0
- package/lib/discord-agent-deps.js +32 -0
- package/lib/endpoint.d.ts +66 -119
- package/lib/endpoint.js +308 -1047
- package/lib/gateway.d.ts +122 -0
- package/lib/gateway.js +235 -0
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -330
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +135 -0
- package/lib/protocol.js +234 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +86 -0
- package/package.json +48 -31
- package/plugin.ts +13 -0
- package/schema.json +63 -0
- package/src/discord-agent-deps.ts +79 -0
- package/src/endpoint.ts +385 -1167
- package/src/gateway.ts +337 -0
- package/src/index.ts +55 -332
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +392 -0
- package/src/webhook.ts +121 -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 -30
- package/dist/index.js +0 -29
- package/lib/adapter.d.ts +0 -25
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -96
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint-interactions.d.ts +0 -34
- package/lib/endpoint-interactions.d.ts.map +0 -1
- package/lib/endpoint-interactions.js +0 -284
- package/lib/endpoint-interactions.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.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 -108
- package/src/endpoint-interactions.ts +0 -354
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -60
- /package/{skills/discord → agent}/PERMITS.md +0 -0
- /package/{skills/discord/SKILL.md → agent/skills/discord.md} +0 -0
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Gateway protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
6
|
+
export function resolveDiscordConfig(config = {}) {
|
|
7
|
+
const entry = config.endpoints?.find((item) => item.context === 'discord' || !item.context);
|
|
8
|
+
const token = (typeof config.token === 'string' && config.token)
|
|
9
|
+
|| (typeof entry?.token === 'string' && entry.token)
|
|
10
|
+
|| process.env.DISCORD_BOT_TOKEN
|
|
11
|
+
|| '';
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new TypeError('Discord adapter requires token (plugins.<key>.token or endpoints with context: discord)');
|
|
14
|
+
}
|
|
15
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
16
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
17
|
+
|| process.env.DISCORD_BOT_NAME
|
|
18
|
+
|| 'discord-bot';
|
|
19
|
+
const connection = config.connection
|
|
20
|
+
?? entry?.connection
|
|
21
|
+
?? 'gateway';
|
|
22
|
+
if (connection === 'interactions') {
|
|
23
|
+
const applicationId = config.applicationId || entry?.applicationId || '';
|
|
24
|
+
const publicKey = config.publicKey || entry?.publicKey || '';
|
|
25
|
+
if (!applicationId || !publicKey) {
|
|
26
|
+
throw new TypeError('Discord connection:interactions requires applicationId and publicKey');
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
context: 'discord',
|
|
30
|
+
connection: 'interactions',
|
|
31
|
+
name,
|
|
32
|
+
token,
|
|
33
|
+
applicationId,
|
|
34
|
+
publicKey,
|
|
35
|
+
interactionsPath: config.interactionsPath || entry?.interactionsPath || '/discord/interactions',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
context: 'discord',
|
|
40
|
+
connection: 'gateway',
|
|
41
|
+
name,
|
|
42
|
+
token,
|
|
43
|
+
intents: config.intents ?? entry?.intents,
|
|
44
|
+
enableSlashCommands: config.enableSlashCommands === true
|
|
45
|
+
|| entry?.enableSlashCommands === true,
|
|
46
|
+
globalCommands: config.globalCommands === true || entry?.globalCommands === true,
|
|
47
|
+
defaultActivity: config.defaultActivity ?? entry?.defaultActivity,
|
|
48
|
+
slashCommands: config.slashCommands ?? entry?.slashCommands,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function resolveChannelKind(channelType) {
|
|
52
|
+
// discord.js ChannelType.DM = 1, GroupDM = 3
|
|
53
|
+
if (channelType === 1 || channelType === 'DM' || channelType === 'private')
|
|
54
|
+
return 'private';
|
|
55
|
+
if (channelType === 3 || channelType === 'GroupDM' || channelType === 'group')
|
|
56
|
+
return 'group';
|
|
57
|
+
return 'channel';
|
|
58
|
+
}
|
|
59
|
+
export function senderDisplayName(msg) {
|
|
60
|
+
return msg.authorName || msg.authorId;
|
|
61
|
+
}
|
|
62
|
+
/** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
|
|
63
|
+
export function formatInboundContent(msg) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (msg.replyToId)
|
|
66
|
+
parts.push(`[reply:${msg.replyToId}]`);
|
|
67
|
+
if (msg.content?.trim())
|
|
68
|
+
parts.push(msg.content.trim());
|
|
69
|
+
for (const attachment of msg.attachments ?? []) {
|
|
70
|
+
const kind = attachment.contentType?.startsWith('image/')
|
|
71
|
+
? 'image'
|
|
72
|
+
: attachment.contentType?.startsWith('audio/')
|
|
73
|
+
? 'audio'
|
|
74
|
+
: attachment.contentType?.startsWith('video/')
|
|
75
|
+
? 'video'
|
|
76
|
+
: 'file';
|
|
77
|
+
const name = attachment.name || attachment.url || 'attachment';
|
|
78
|
+
parts.push(`[${kind}: ${name}]`);
|
|
79
|
+
}
|
|
80
|
+
for (const title of msg.embedTitles ?? []) {
|
|
81
|
+
parts.push(`[embed: ${title}]`);
|
|
82
|
+
}
|
|
83
|
+
for (const name of msg.stickerNames ?? []) {
|
|
84
|
+
parts.push(`[sticker: ${name}]`);
|
|
85
|
+
}
|
|
86
|
+
const text = parts.join('\n').trim();
|
|
87
|
+
return text || '(Empty message)';
|
|
88
|
+
}
|
|
89
|
+
export function formatButtonContent(interaction) {
|
|
90
|
+
return `[action: ${interaction.customId}]`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Wire-encode an already-rendered outbound payload into Discord message body.
|
|
94
|
+
* Segment canonicalization is intentionally not done here.
|
|
95
|
+
*/
|
|
96
|
+
export function formatOutboundBody(payload) {
|
|
97
|
+
if (typeof payload === 'string') {
|
|
98
|
+
return { content: payload };
|
|
99
|
+
}
|
|
100
|
+
const segments = Array.isArray(payload)
|
|
101
|
+
? payload
|
|
102
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
103
|
+
? [payload]
|
|
104
|
+
: [];
|
|
105
|
+
if (segments.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
content: payload == null
|
|
108
|
+
? ''
|
|
109
|
+
: typeof payload === 'object'
|
|
110
|
+
? JSON.stringify(payload)
|
|
111
|
+
: String(payload),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
let content = '';
|
|
115
|
+
const embeds = [];
|
|
116
|
+
const files = [];
|
|
117
|
+
let components;
|
|
118
|
+
for (const item of segments) {
|
|
119
|
+
if (typeof item === 'string') {
|
|
120
|
+
content += item;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const data = item.data ?? {};
|
|
124
|
+
switch (item.type) {
|
|
125
|
+
case 'text':
|
|
126
|
+
content += String(data.text ?? data.content ?? '');
|
|
127
|
+
break;
|
|
128
|
+
case 'at':
|
|
129
|
+
content += `<@${String(data.id ?? '')}>`;
|
|
130
|
+
break;
|
|
131
|
+
case 'channel_mention':
|
|
132
|
+
content += `<#${String(data.id ?? '')}>`;
|
|
133
|
+
break;
|
|
134
|
+
case 'role_mention':
|
|
135
|
+
content += `<@&${String(data.id ?? '')}>`;
|
|
136
|
+
break;
|
|
137
|
+
case 'emoji':
|
|
138
|
+
content += data.animated
|
|
139
|
+
? `<a:${String(data.name)}:${String(data.id)}>`
|
|
140
|
+
: `<:${String(data.name)}:${String(data.id)}>`;
|
|
141
|
+
break;
|
|
142
|
+
case 'image':
|
|
143
|
+
case 'audio':
|
|
144
|
+
case 'video':
|
|
145
|
+
case 'file': {
|
|
146
|
+
const name = String(data.name || data.filename || item.type);
|
|
147
|
+
if (typeof data.file === 'string' && data.file) {
|
|
148
|
+
files.push({ name, file: data.file });
|
|
149
|
+
}
|
|
150
|
+
else if (typeof data.url === 'string' && data.url) {
|
|
151
|
+
files.push({ name, url: data.url });
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'embed':
|
|
156
|
+
embeds.push({ ...data });
|
|
157
|
+
break;
|
|
158
|
+
case 'keyboard': {
|
|
159
|
+
const rows = (data.rows ?? []);
|
|
160
|
+
components = rows.map((row) => ({
|
|
161
|
+
type: 1,
|
|
162
|
+
components: row.map((btn) => ({
|
|
163
|
+
type: 2,
|
|
164
|
+
custom_id: String(btn.payload).slice(0, 100),
|
|
165
|
+
label: btn.label,
|
|
166
|
+
style: btn.style === 'danger' ? 4 : btn.style === 'primary' ? 1 : 2,
|
|
167
|
+
disabled: !!btn.disabled,
|
|
168
|
+
})),
|
|
169
|
+
}));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
if (data.text != null)
|
|
174
|
+
content += String(data.text);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
...(content.trim() ? { content: content.trim() } : {}),
|
|
180
|
+
...(embeds.length > 0 ? { embeds: embeds.slice(0, 10) } : {}),
|
|
181
|
+
...(files.length > 0 ? { files } : {}),
|
|
182
|
+
...(components ? { components } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function activityTypeCode(type) {
|
|
186
|
+
const map = {
|
|
187
|
+
PLAYING: 0,
|
|
188
|
+
STREAMING: 1,
|
|
189
|
+
LISTENING: 2,
|
|
190
|
+
WATCHING: 3,
|
|
191
|
+
COMPETING: 5,
|
|
192
|
+
};
|
|
193
|
+
return map[type] ?? 0;
|
|
194
|
+
}
|
|
195
|
+
export function verifyDiscordInteractionSignature(publicKeyHex, body, signature, timestamp) {
|
|
196
|
+
if (!publicKeyHex || !signature || !timestamp)
|
|
197
|
+
return false;
|
|
198
|
+
try {
|
|
199
|
+
const key = createPublicKey({
|
|
200
|
+
key: Buffer.concat([
|
|
201
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
202
|
+
Buffer.from(publicKeyHex, 'hex'),
|
|
203
|
+
]),
|
|
204
|
+
format: 'der',
|
|
205
|
+
type: 'spki',
|
|
206
|
+
});
|
|
207
|
+
return cryptoVerify(null, Buffer.from(timestamp + body), key, Buffer.from(signature, 'hex'));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export function formatSlashCommandContent(interaction) {
|
|
214
|
+
const data = interaction.data;
|
|
215
|
+
const parts = [`/${data?.name ?? 'command'}`];
|
|
216
|
+
for (const opt of data?.options ?? []) {
|
|
217
|
+
parts.push(`${opt.name}:${String(opt.value)}`);
|
|
218
|
+
}
|
|
219
|
+
return parts.join(' ');
|
|
220
|
+
}
|
|
221
|
+
export function interactionToInboundMessage(interaction) {
|
|
222
|
+
const user = interaction.member?.user
|
|
223
|
+
?? interaction.user;
|
|
224
|
+
return {
|
|
225
|
+
id: String(interaction.id),
|
|
226
|
+
content: formatSlashCommandContent(interaction),
|
|
227
|
+
channelId: String(interaction.channel_id ?? ''),
|
|
228
|
+
channelKind: interaction.guild_id ? 'channel' : 'private',
|
|
229
|
+
authorId: String(user?.id ?? ''),
|
|
230
|
+
authorName: String(user?.username ?? user?.id ?? ''),
|
|
231
|
+
createdTimestamp: Date.now(),
|
|
232
|
+
guildId: interaction.guild_id != null ? String(interaction.guild_id) : undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
package/lib/webhook.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord interactions HTTP: signature → parse → admit.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { type DiscordInboundMessage, type ResolvedDiscordInteractionsConfig } from './protocol.js';
|
|
7
|
+
export interface DiscordInteractionsHandler {
|
|
8
|
+
readonly config: ResolvedDiscordInteractionsConfig;
|
|
9
|
+
readonly isOpen: boolean;
|
|
10
|
+
admit(msg: DiscordInboundMessage): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerDiscordInteractionRoutes(http: HttpHost, handler: DiscordInteractionsHandler): HttpRouteRegistration[];
|
|
13
|
+
export declare function handleDiscordInteractionRequest(request: IncomingMessage, response: ServerResponse, handler: DiscordInteractionsHandler): Promise<void>;
|
package/lib/webhook.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { interactionToInboundMessage, verifyDiscordInteractionSignature, } from './protocol.js';
|
|
3
|
+
const logger = getLogger('discord');
|
|
4
|
+
const INTERACTION_TYPE_PING = 1;
|
|
5
|
+
const INTERACTION_TYPE_APPLICATION_COMMAND = 2;
|
|
6
|
+
const INTERACTION_RESPONSE_PONG = 1;
|
|
7
|
+
const INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE = 4;
|
|
8
|
+
/** EPHEHEMERAL — 仅发起者可见(对齐旧 endpoint-interactions 默认响应)。 */
|
|
9
|
+
const INTERACTION_FLAG_EPHEMERAL = 64;
|
|
10
|
+
export function registerDiscordInteractionRoutes(http, handler) {
|
|
11
|
+
const path = handler.config.interactionsPath;
|
|
12
|
+
return [
|
|
13
|
+
http.route('POST', path, async (request, response) => {
|
|
14
|
+
await handleDiscordInteractionRequest(request, response, handler);
|
|
15
|
+
}, { summary: 'Discord interactions callback', tags: ['discord'] }),
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
export async function handleDiscordInteractionRequest(request, response, handler) {
|
|
19
|
+
try {
|
|
20
|
+
const signature = headerValue(request.headers['x-signature-ed25519']);
|
|
21
|
+
const timestamp = headerValue(request.headers['x-signature-timestamp']);
|
|
22
|
+
const rawBody = await readInteractionBody(request);
|
|
23
|
+
if (!signature || !timestamp) {
|
|
24
|
+
response.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
25
|
+
response.end('Unauthorized');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!verifyDiscordInteractionSignature(handler.config.publicKey, rawBody, signature, timestamp)) {
|
|
29
|
+
response.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
30
|
+
response.end('Unauthorized');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const interaction = JSON.parse(rawBody);
|
|
34
|
+
if (interaction.type === INTERACTION_TYPE_PING) {
|
|
35
|
+
writeJson(response, 200, { type: INTERACTION_RESPONSE_PONG });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (interaction.type === INTERACTION_TYPE_APPLICATION_COMMAND) {
|
|
39
|
+
if (handler.isOpen) {
|
|
40
|
+
handler.admit(interactionToInboundMessage(interaction));
|
|
41
|
+
}
|
|
42
|
+
// 即时响应(type 4):defer(type 5) 需要 followup PATCH,未实现会让用户端一直转圈
|
|
43
|
+
const commandName = String(interaction.data?.name ?? '');
|
|
44
|
+
writeJson(response, 200, {
|
|
45
|
+
type: INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
|
|
46
|
+
data: {
|
|
47
|
+
content: `处理命令: ${commandName}`,
|
|
48
|
+
flags: INTERACTION_FLAG_EPHEMERAL,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
response.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
54
|
+
response.end('Unsupported interaction type');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error('Discord interactions error:', error);
|
|
58
|
+
if (!response.headersSent) {
|
|
59
|
+
response.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
60
|
+
response.end('Internal Server Error');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function headerValue(value) {
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
return value[0] ?? '';
|
|
67
|
+
return value ?? '';
|
|
68
|
+
}
|
|
69
|
+
async function readInteractionBody(request) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
for await (const chunk of request) {
|
|
73
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
74
|
+
size += buffer.length;
|
|
75
|
+
if (size > 1_048_576) {
|
|
76
|
+
request.destroy();
|
|
77
|
+
throw new Error('Request body exceeds 1MB');
|
|
78
|
+
}
|
|
79
|
+
chunks.push(buffer);
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
82
|
+
}
|
|
83
|
+
function writeJson(response, status, body) {
|
|
84
|
+
response.writeHead(status, { 'Content-Type': 'application/json' });
|
|
85
|
+
response.end(JSON.stringify(body));
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-discord",
|
|
3
|
-
"version": "5.0.
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "5.0.3",
|
|
4
|
+
"description": "Zhin.js Discord adapter for Plugin Runtime (Gateway WebSocket)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -33,45 +33,48 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"discord.js": "^14.26.4",
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@types/react-dom": "^19.2.3",
|
|
42
|
-
"lucide-react": "^1.22.0",
|
|
43
|
-
"typescript": "^6.0.3",
|
|
44
|
-
"@zhin.js/cli": "1.0.92",
|
|
45
|
-
"@zhin.js/client": "2.0.4",
|
|
46
|
-
"@zhin.js/contract": "1.0.2",
|
|
47
|
-
"@zhin.js/host-api": "2.0.4",
|
|
48
|
-
"@zhin.js/host-router": "2.0.2",
|
|
49
|
-
"zhin.js": "4.1.1"
|
|
36
|
+
"@zhin.js/adapter": "1.0.1",
|
|
37
|
+
"@zhin.js/core": "1.3.5",
|
|
38
|
+
"@zhin.js/host-http": "1.0.1",
|
|
39
|
+
"@zhin.js/logger": "1.0.75",
|
|
40
|
+
"@zhin.js/plugin-runtime": "1.0.1"
|
|
50
41
|
},
|
|
51
42
|
"peerDependencies": {
|
|
52
|
-
"
|
|
53
|
-
"@zhin.js/
|
|
54
|
-
"@zhin.js/
|
|
55
|
-
"@zhin.js/
|
|
56
|
-
"zhin.js": "
|
|
43
|
+
"zod": "^4.0.0",
|
|
44
|
+
"@zhin.js/adapter": "1.0.1",
|
|
45
|
+
"@zhin.js/agent": "1.0.4",
|
|
46
|
+
"@zhin.js/core": "1.3.5",
|
|
47
|
+
"@zhin.js/plugin-runtime": "1.0.1",
|
|
48
|
+
"zhin.js": "4.1.3"
|
|
57
49
|
},
|
|
58
50
|
"peerDependenciesMeta": {
|
|
59
|
-
"
|
|
51
|
+
"zhin.js": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@zhin.js/agent": {
|
|
60
55
|
"optional": true
|
|
61
56
|
},
|
|
62
|
-
"
|
|
57
|
+
"zod": {
|
|
63
58
|
"optional": true
|
|
64
59
|
}
|
|
65
60
|
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^26.1.0",
|
|
63
|
+
"typescript": "^6.0.3",
|
|
64
|
+
"vitest": "^4.1.10",
|
|
65
|
+
"zod": "^4.4.3",
|
|
66
|
+
"@zhin.js/agent": "1.0.4",
|
|
67
|
+
"@zhin.js/host-http": "1.0.1",
|
|
68
|
+
"zhin.js": "4.1.3"
|
|
69
|
+
},
|
|
66
70
|
"files": [
|
|
71
|
+
"adapters",
|
|
72
|
+
"plugin.ts",
|
|
73
|
+
"schema.json",
|
|
67
74
|
"src",
|
|
68
75
|
"lib",
|
|
69
|
-
"
|
|
70
|
-
"dist",
|
|
71
|
-
"skills",
|
|
72
|
-
"plugin.yml",
|
|
76
|
+
"agent",
|
|
73
77
|
"README.md",
|
|
74
|
-
"node",
|
|
75
78
|
"CHANGELOG.md"
|
|
76
79
|
],
|
|
77
80
|
"publishConfig": {
|
|
@@ -81,9 +84,23 @@
|
|
|
81
84
|
"engines": {
|
|
82
85
|
"node": "^20.19.0 || >=22.12.0"
|
|
83
86
|
},
|
|
87
|
+
"zhin": {
|
|
88
|
+
"protocol": 1,
|
|
89
|
+
"type": "plugin",
|
|
90
|
+
"entry": "./plugin.ts",
|
|
91
|
+
"engine": "^1.0.0",
|
|
92
|
+
"runtime": "trusted",
|
|
93
|
+
"features": [
|
|
94
|
+
{
|
|
95
|
+
"package": "@zhin.js/adapter",
|
|
96
|
+
"api": "^1.0.0"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"plugins": []
|
|
100
|
+
},
|
|
84
101
|
"scripts": {
|
|
85
|
-
"build": "
|
|
86
|
-
"clean": "rimraf lib
|
|
87
|
-
"
|
|
102
|
+
"build": "tsc",
|
|
103
|
+
"clean": "rimraf lib",
|
|
104
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/discord/tests"
|
|
88
105
|
}
|
|
89
106
|
}
|
package/plugin.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { registerDiscordPlatformPermitChecker } from './src/platform-permit.js';
|
|
3
|
+
|
|
4
|
+
export default definePlugin({
|
|
5
|
+
name: 'discord',
|
|
6
|
+
metadata: {
|
|
7
|
+
displayName: 'Discord Gateway Adapter',
|
|
8
|
+
},
|
|
9
|
+
setup() {
|
|
10
|
+
// 平台权限门禁:guild_owner / moderate_members 等(agent 工具 platformPermit)
|
|
11
|
+
return registerDiscordPlatformPermitChecker();
|
|
12
|
+
},
|
|
13
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"default": "discord-bot"
|
|
9
|
+
},
|
|
10
|
+
"token": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"connection": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"enum": ["gateway", "interactions"],
|
|
16
|
+
"default": "gateway",
|
|
17
|
+
"description": "Gateway WebSocket (default). interactions uses httpHostToken POST + Ed25519 verify."
|
|
18
|
+
},
|
|
19
|
+
"intents": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "number" }
|
|
22
|
+
},
|
|
23
|
+
"enableSlashCommands": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"default": false
|
|
26
|
+
},
|
|
27
|
+
"globalCommands": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": false
|
|
30
|
+
},
|
|
31
|
+
"defaultActivity": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"properties": {
|
|
35
|
+
"name": { "type": "string" },
|
|
36
|
+
"type": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "COMPETING"]
|
|
39
|
+
},
|
|
40
|
+
"url": { "type": "string" }
|
|
41
|
+
},
|
|
42
|
+
"required": ["name", "type"]
|
|
43
|
+
},
|
|
44
|
+
"slashCommands": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "type": "object" }
|
|
47
|
+
},
|
|
48
|
+
"applicationId": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Required when connection is interactions."
|
|
51
|
+
},
|
|
52
|
+
"publicKey": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Required when connection is interactions (hex Ed25519 public key)."
|
|
55
|
+
},
|
|
56
|
+
"interactionsPath": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"default": "/discord/interactions",
|
|
59
|
+
"description": "POST path on httpHostToken when connection is interactions."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"required": ["token"]
|
|
63
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for discord.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DiscordAgentEndpoint {
|
|
7
|
+
addRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
|
|
8
|
+
removeRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
|
|
9
|
+
getRoles(guildId: string): Promise<unknown[]>;
|
|
10
|
+
createThread(
|
|
11
|
+
channelId: string,
|
|
12
|
+
name: string,
|
|
13
|
+
messageId?: string,
|
|
14
|
+
autoArchiveDuration?: number,
|
|
15
|
+
): Promise<{ id: string }>;
|
|
16
|
+
addReaction(channelId: string, messageId: string, emoji: string): Promise<void>;
|
|
17
|
+
sendEmbed(
|
|
18
|
+
channelId: string,
|
|
19
|
+
embedData: Record<string, unknown>,
|
|
20
|
+
): Promise<{ id: string }>;
|
|
21
|
+
createForumPost(
|
|
22
|
+
channelId: string,
|
|
23
|
+
name: string,
|
|
24
|
+
content: string,
|
|
25
|
+
tags?: string[],
|
|
26
|
+
): Promise<{ id: string }>;
|
|
27
|
+
kickMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
28
|
+
banMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
29
|
+
unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
30
|
+
timeoutMember(
|
|
31
|
+
guildId: string,
|
|
32
|
+
userId: string,
|
|
33
|
+
duration?: number,
|
|
34
|
+
reason?: string,
|
|
35
|
+
): Promise<boolean>;
|
|
36
|
+
setNickname(guildId: string, userId: string, nickname: string): Promise<boolean>;
|
|
37
|
+
getMembers(guildId: string, limit?: number): Promise<unknown[]>;
|
|
38
|
+
getGuildInfo(guildId: string): Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DiscordAgentDeps {
|
|
42
|
+
getEndpoint: (endpointId: string) => DiscordAgentEndpoint;
|
|
43
|
+
/** Alias kept for existing agent/tools that call getGatewayEndpoint. */
|
|
44
|
+
getGatewayEndpoint: (endpointId: string) => DiscordAgentEndpoint;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const endpoints = new Map<string, DiscordAgentEndpoint>();
|
|
48
|
+
let override: DiscordAgentDeps | null = null;
|
|
49
|
+
|
|
50
|
+
export function registerDiscordAgentEndpoint(
|
|
51
|
+
endpointId: string,
|
|
52
|
+
endpoint: DiscordAgentEndpoint,
|
|
53
|
+
): () => void {
|
|
54
|
+
endpoints.set(endpointId, endpoint);
|
|
55
|
+
return () => {
|
|
56
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
57
|
+
endpoints.delete(endpointId);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
63
|
+
export function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void {
|
|
64
|
+
override = deps;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lookup(endpointId: string): DiscordAgentEndpoint {
|
|
68
|
+
const registered = endpoints.get(endpointId);
|
|
69
|
+
if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
70
|
+
return registered;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getDiscordAgentDeps(): DiscordAgentDeps {
|
|
74
|
+
if (override) return override;
|
|
75
|
+
return {
|
|
76
|
+
getEndpoint: lookup,
|
|
77
|
+
getGatewayEndpoint: lookup,
|
|
78
|
+
};
|
|
79
|
+
}
|