@zhin.js/adapter-discord 5.0.2 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +49 -79
  3. package/adapters/discord.ts +46 -0
  4. package/agent/tools/add_role.ts +2 -2
  5. package/agent/tools/create_thread.ts +2 -2
  6. package/agent/tools/forum_post.ts +2 -2
  7. package/agent/tools/list_roles.ts +2 -2
  8. package/agent/tools/react.ts +2 -2
  9. package/agent/tools/remove_role.ts +2 -2
  10. package/agent/tools/send_embed.ts +2 -2
  11. package/lib/discord-agent-deps.d.ts +35 -0
  12. package/lib/discord-agent-deps.js +32 -0
  13. package/lib/endpoint.d.ts +71 -0
  14. package/lib/endpoint.js +355 -0
  15. package/lib/gateway.d.ts +122 -0
  16. package/lib/gateway.js +235 -0
  17. package/lib/index.d.ts +6 -0
  18. package/lib/index.js +6 -0
  19. package/lib/platform-permit.d.ts +15 -0
  20. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  21. package/lib/protocol.d.ts +135 -0
  22. package/lib/protocol.js +234 -0
  23. package/lib/webhook.d.ts +13 -0
  24. package/lib/webhook.js +86 -0
  25. package/package.json +43 -36
  26. package/plugin.ts +13 -0
  27. package/schema.json +105 -0
  28. package/src/discord-agent-deps.ts +68 -11
  29. package/src/endpoint.ts +385 -1167
  30. package/src/gateway.ts +337 -0
  31. package/src/index.ts +56 -157
  32. package/src/platform-permit.ts +1 -1
  33. package/src/protocol.ts +392 -0
  34. package/src/webhook.ts +121 -0
  35. package/client/Dashboard.tsx +0 -195
  36. package/client/index.tsx +0 -11
  37. package/client/tsconfig.json +0 -7
  38. package/client/utils/api.ts +0 -30
  39. package/dist/index.js +0 -29
  40. package/lib/agent/tools/add_role.js +0 -22
  41. package/lib/agent/tools/add_role.js.map +0 -1
  42. package/lib/agent/tools/create_thread.js +0 -23
  43. package/lib/agent/tools/create_thread.js.map +0 -1
  44. package/lib/agent/tools/forum_post.js +0 -22
  45. package/lib/agent/tools/forum_post.js.map +0 -1
  46. package/lib/agent/tools/list_roles.js +0 -20
  47. package/lib/agent/tools/list_roles.js.map +0 -1
  48. package/lib/agent/tools/react.js +0 -20
  49. package/lib/agent/tools/react.js.map +0 -1
  50. package/lib/agent/tools/remove_role.js +0 -22
  51. package/lib/agent/tools/remove_role.js.map +0 -1
  52. package/lib/agent/tools/send_embed.js +0 -40
  53. package/lib/agent/tools/send_embed.js.map +0 -1
  54. package/lib/src/adapter.js +0 -96
  55. package/lib/src/adapter.js.map +0 -1
  56. package/lib/src/discord-agent-deps.js +0 -10
  57. package/lib/src/discord-agent-deps.js.map +0 -1
  58. package/lib/src/endpoint-interactions.js +0 -284
  59. package/lib/src/endpoint-interactions.js.map +0 -1
  60. package/lib/src/endpoint.js +0 -1093
  61. package/lib/src/endpoint.js.map +0 -1
  62. package/lib/src/index.js +0 -159
  63. package/lib/src/index.js.map +0 -1
  64. package/lib/src/platform-permit.js.map +0 -1
  65. package/lib/src/segment-mapper.js +0 -2
  66. package/lib/src/segment-mapper.js.map +0 -1
  67. package/lib/src/types.js +0 -2
  68. package/lib/src/types.js.map +0 -1
  69. package/plugin.yml +0 -3
  70. package/src/adapter.ts +0 -108
  71. package/src/endpoint-interactions.ts +0 -354
  72. package/src/segment-mapper.ts +0 -1
  73. package/src/types.ts +0 -60
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.2",
4
- "description": "Zhin.js adapter for Discord",
3
+ "version": "6.0.0",
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,55 +33,48 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "discord.js": "^14.26.4",
36
- "tweetnacl": "^1.0.3"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^26.1.0",
40
- "@types/react": "^19.2.17",
41
- "@types/react-dom": "^19.2.3",
42
- "lucide-react": "^1.22.0",
43
- "typescript": "^6.0.3",
44
- "zod": "^4.4.3",
45
- "@zhin.js/cli": "1.0.93",
46
- "@zhin.js/client": "2.0.5",
47
- "@zhin.js/contract": "1.0.3",
48
- "@zhin.js/host-api": "2.0.5",
49
- "@zhin.js/host-router": "2.0.3",
50
- "zhin.js": "4.1.2",
51
- "@zhin.js/agent": "1.0.3"
36
+ "@zhin.js/adapter": "1.1.0",
37
+ "@zhin.js/core": "1.4.0",
38
+ "@zhin.js/host-http": "1.0.2",
39
+ "@zhin.js/logger": "1.0.75",
40
+ "@zhin.js/plugin-runtime": "1.1.0"
52
41
  },
53
42
  "peerDependencies": {
54
43
  "zod": "^4.0.0",
55
- "@zhin.js/client": "2.0.5",
56
- "@zhin.js/host-api": "2.0.5",
57
- "@zhin.js/contract": "1.0.3",
58
- "@zhin.js/host-router": "2.0.3",
59
- "zhin.js": "4.1.2",
60
- "@zhin.js/agent": "1.0.3"
44
+ "@zhin.js/adapter": "1.1.0",
45
+ "@zhin.js/agent": "1.0.5",
46
+ "@zhin.js/core": "1.4.0",
47
+ "@zhin.js/plugin-runtime": "1.1.0",
48
+ "zhin.js": "5.0.0"
61
49
  },
62
50
  "peerDependenciesMeta": {
63
- "@zhin.js/agent": {
64
- "optional": true
65
- },
66
- "@zhin.js/client": {
51
+ "zhin.js": {
67
52
  "optional": true
68
53
  },
69
- "@zhin.js/host-api": {
54
+ "@zhin.js/agent": {
70
55
  "optional": true
71
56
  },
72
57
  "zod": {
73
58
  "optional": true
74
59
  }
75
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.5",
67
+ "@zhin.js/host-http": "1.0.2",
68
+ "zhin.js": "5.0.0"
69
+ },
76
70
  "files": [
71
+ "adapters",
72
+ "plugin.ts",
73
+ "schema.json",
77
74
  "src",
78
75
  "lib",
79
- "client",
80
- "dist",
81
76
  "agent",
82
- "plugin.yml",
83
77
  "README.md",
84
- "node",
85
78
  "CHANGELOG.md"
86
79
  ],
87
80
  "publishConfig": {
@@ -91,9 +84,23 @@
91
84
  "engines": {
92
85
  "node": "^20.19.0 || >=22.12.0"
93
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
+ },
94
101
  "scripts": {
95
- "build": "zhin build",
96
- "clean": "rimraf lib dist",
97
- "build:node": "tsc"
102
+ "build": "tsc",
103
+ "clean": "rimraf lib",
104
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/discord/tests"
98
105
  }
99
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,105 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "connection": {
7
+ "type": "string",
8
+ "enum": [
9
+ "gateway",
10
+ "interactions"
11
+ ],
12
+ "default": "gateway",
13
+ "description": "Gateway WebSocket (default). interactions uses httpHostToken POST + Ed25519 verify."
14
+ },
15
+ "intents": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "number"
19
+ }
20
+ },
21
+ "enableSlashCommands": {
22
+ "type": "boolean",
23
+ "default": false
24
+ },
25
+ "globalCommands": {
26
+ "type": "boolean",
27
+ "default": false
28
+ },
29
+ "defaultActivity": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "properties": {
33
+ "name": {
34
+ "type": "string"
35
+ },
36
+ "type": {
37
+ "type": "string",
38
+ "enum": [
39
+ "PLAYING",
40
+ "STREAMING",
41
+ "LISTENING",
42
+ "WATCHING",
43
+ "COMPETING"
44
+ ]
45
+ },
46
+ "url": {
47
+ "type": "string"
48
+ }
49
+ },
50
+ "required": [
51
+ "name",
52
+ "type"
53
+ ]
54
+ },
55
+ "slashCommands": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "object"
59
+ }
60
+ },
61
+ "applicationId": {
62
+ "type": "string",
63
+ "description": "Required when connection is interactions."
64
+ },
65
+ "publicKey": {
66
+ "type": "string",
67
+ "description": "Required when connection is interactions (hex Ed25519 public key)."
68
+ },
69
+ "interactionsPath": {
70
+ "type": "string",
71
+ "default": "/discord/interactions",
72
+ "description": "POST path on httpHostToken when connection is interactions."
73
+ },
74
+ "endpoints": {
75
+ "type": "array",
76
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
77
+ "items": {
78
+ "type": "object",
79
+ "additionalProperties": true,
80
+ "properties": {
81
+ "name": {
82
+ "type": "string",
83
+ "description": "Discord bot name"
84
+ },
85
+ "token": {
86
+ "type": "string",
87
+ "description": "Discord bot token"
88
+ }
89
+ },
90
+ "required": [
91
+ "name",
92
+ "token"
93
+ ]
94
+ }
95
+ },
96
+ "commandPrefix": {
97
+ "type": "string",
98
+ "default": "",
99
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
100
+ }
101
+ },
102
+ "required": [
103
+ "endpoints"
104
+ ]
105
+ }
@@ -1,22 +1,79 @@
1
1
  /**
2
- * Shared runtime deps for discord agent/ authoring tools.
3
- * Set once from plugin index during init never call getPlugin() from tool execute.
2
+ * Agent tool deps for discord.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
4
  */
5
- import type { DiscordAdapter, DiscordEndpointLike } from './adapter.js';
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
+ }
6
40
 
7
41
  export interface DiscordAgentDeps {
8
- getEndpoint: (endpointId: string) => DiscordEndpointLike;
9
- getGatewayEndpoint: (endpointId: string) => DiscordEndpointLike;
10
- getAdapter: () => DiscordAdapter;
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
+ };
11
60
  }
12
61
 
13
- let _deps: DiscordAgentDeps | null = null;
62
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
63
+ export function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void {
64
+ override = deps;
65
+ }
14
66
 
15
- export function setDiscordAgentDeps(deps: DiscordAgentDeps): void {
16
- _deps = deps;
67
+ function lookup(endpointId: string): DiscordAgentEndpoint {
68
+ const registered = endpoints.get(endpointId);
69
+ if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
70
+ return registered;
17
71
  }
18
72
 
19
73
  export function getDiscordAgentDeps(): DiscordAgentDeps {
20
- if (!_deps) throw new Error('discord agent deps not initialized');
21
- return _deps;
74
+ if (override) return override;
75
+ return {
76
+ getEndpoint: lookup,
77
+ getGatewayEndpoint: lookup,
78
+ };
22
79
  }