@zhin.js/adapter-discord 7.0.2 → 7.0.5

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 CHANGED
@@ -1,5 +1,100 @@
1
1
  # @zhin.js/adapter-discord
2
2
 
3
+ ## 7.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 51015d6: refactor(adapters): migrate platform-permit to PermissionSubject
8
+
9
+ All adapter platform-permit checkers now accept PermissionSubject
10
+ (duck-typed from Message/CommandSession) instead of Message directly.
11
+ registerPlatformPermitChecker is replaced by host.registerPlatform in
12
+ the adapter plugin setup. activity-feedback service updated for new
13
+ PermissionHost integration.
14
+
15
+ - Updated dependencies [6f9c366]
16
+ - Updated dependencies [373a56b]
17
+ - Updated dependencies [0de46a8]
18
+ - Updated dependencies [c106ecc]
19
+ - Updated dependencies [ca92e03]
20
+ - Updated dependencies [b0f37ae]
21
+ - Updated dependencies [ba08a2f]
22
+ - Updated dependencies [b08f7fe]
23
+ - Updated dependencies [daffd4c]
24
+ - Updated dependencies [36c7400]
25
+ - Updated dependencies [a9fa72e]
26
+ - Updated dependencies [c8de3ef]
27
+ - Updated dependencies [60f0fc8]
28
+ - Updated dependencies [574c990]
29
+ - Updated dependencies [d096f16]
30
+ - Updated dependencies [3f29623]
31
+ - Updated dependencies [2916852]
32
+ - Updated dependencies [d047869]
33
+ - Updated dependencies [162fa34]
34
+ - Updated dependencies [e62561e]
35
+ - Updated dependencies [3eeeb46]
36
+ - Updated dependencies [85d0f82]
37
+ - Updated dependencies [61bfc1c]
38
+ - Updated dependencies [04bad47]
39
+ - Updated dependencies [e40b048]
40
+ - Updated dependencies [5a5b1bb]
41
+ - Updated dependencies [f1708c3]
42
+ - Updated dependencies [d254a81]
43
+ - Updated dependencies [7123c47]
44
+ - Updated dependencies [05befc1]
45
+ - Updated dependencies [0663b6a]
46
+ - Updated dependencies [f28f9b3]
47
+ - Updated dependencies [9f340f7]
48
+ - Updated dependencies [e53444f]
49
+ - Updated dependencies [5eedd26]
50
+ - Updated dependencies [92b0dd7]
51
+ - Updated dependencies [f919b6f]
52
+ - Updated dependencies [098e411]
53
+ - Updated dependencies [e1b7c01]
54
+ - Updated dependencies [9b94f87]
55
+ - Updated dependencies [a7df753]
56
+ - @zhin.js/agent@1.1.5
57
+ - @zhin.js/host-http@1.0.8
58
+ - @zhin.js/permission@1.0.1
59
+ - @zhin.js/im-contract@1.0.3
60
+ - @zhin.js/adapter@1.1.7
61
+ - @zhin.js/plugin-runtime@1.1.5
62
+ - @zhin.js/command@1.0.9
63
+ - @zhin.js/core@1.5.4
64
+ - zhin.js@6.0.4
65
+
66
+ ## 7.0.4
67
+
68
+ ### Patch Changes
69
+
70
+ - f8c7a54: fix: im
71
+ - Updated dependencies [f8c7a54]
72
+ - @zhin.js/logger@1.0.76
73
+ - @zhin.js/host-http@1.0.7
74
+ - @zhin.js/adapter@1.1.6
75
+ - @zhin.js/agent@1.1.4
76
+ - @zhin.js/command@1.0.8
77
+ - @zhin.js/core@1.5.3
78
+ - @zhin.js/im-contract@1.0.2
79
+ - @zhin.js/plugin-runtime@1.1.4
80
+ - zhin.js@6.0.3
81
+
82
+ ## 7.0.3
83
+
84
+ ### Patch Changes
85
+
86
+ - Updated dependencies [afc0e66]
87
+ - Updated dependencies [2e41ad5]
88
+ - Updated dependencies [9f57124]
89
+ - @zhin.js/core@1.5.2
90
+ - @zhin.js/adapter@1.1.5
91
+ - @zhin.js/im-contract@1.0.1
92
+ - @zhin.js/plugin-runtime@1.1.3
93
+ - @zhin.js/command@1.0.7
94
+ - @zhin.js/host-http@1.0.6
95
+ - @zhin.js/agent@1.1.3
96
+ - zhin.js@6.0.2
97
+
3
98
  ## 7.0.2
4
99
 
5
100
  ### Patch Changes
package/README.md CHANGED
@@ -7,7 +7,7 @@ Zhin.js Discord 适配器(Plugin Runtime),默认通过 **Gateway WebSocket
7
7
  - Gateway WebSocket 入站(默认;无需公网 HTTPS / host)
8
8
  - 解析 text / mention / attachment / embed / sticker / button
9
9
  - 支持私聊、群组与服务器频道
10
- - 出站 `send({ target, payload })` → Discord channel message(text / media / embed / keyboard)
10
+ - 出站 `send({ conversation, payload })` → Discord channel message(text / media / embed / keyboard)
11
11
  - 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
12
12
  - Interactions HTTP webhook 延期(需 `httpHostToken`);配置 `connection: interactions` 会明确报错
13
13
 
@@ -25,8 +25,8 @@ pnpm add @zhin.js/adapter-discord discord.js
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
26
26
  - **无需** `@zhin.js/host-http` / `@zhin.js/host-router`(Gateway 路径)
27
27
 
28
- 入站:`gateway.receive({ adapter, target: channelId, content: text, sender, metadata })`
29
- 出站:`send({ target, payload })` → discord.js channel.send
28
+ 入站:`gateway.receive({ conversation, message, content: text, sender, metadata })`(`conversation` 为 `ConversationRef`,guild 频道带 `parent` guild 容器)
29
+ 出站:`send({ conversation, payload })` → discord.js channel.send
30
30
 
31
31
  ### 平台权限(platform permit)
32
32
 
@@ -21,8 +21,8 @@ export default defineAdapter({
21
21
  const config = resolveDiscordConfig(context.config);
22
22
  const gateway = context.use(messageGatewayToken);
23
23
  // 注册到插件运行时状态(discord.endpoint list 的"运行中"数据源)
24
- context.use(discordRuntimeStateToken).endpoints.set(config.name, {
25
- name: config.name,
24
+ context.use(discordRuntimeStateToken).endpoints.set(config.id, {
25
+ id: config.id,
26
26
  mode: config.connection,
27
27
  });
28
28
  if (config.connection === 'interactions') {
@@ -37,8 +37,8 @@ export default defineAdapter<DiscordAdapterConfig>({
37
37
  const config = resolveDiscordConfig(context.config);
38
38
  const gateway = context.use(messageGatewayToken);
39
39
  // 注册到插件运行时状态(discord.endpoint list 的"运行中"数据源)
40
- context.use(discordRuntimeStateToken).endpoints.set(config.name, {
41
- name: config.name,
40
+ context.use(discordRuntimeStateToken).endpoints.set(config.id, {
41
+ id: config.id,
42
42
  mode: config.connection,
43
43
  });
44
44
  if (config.connection === 'interactions') {
@@ -25,11 +25,11 @@ export interface DiscordAgentEndpoint {
25
25
  getGuildInfo(guildId: string): Promise<unknown>;
26
26
  }
27
27
  export interface DiscordAgentDeps {
28
- getEndpoint: (endpointId: string) => DiscordAgentEndpoint;
28
+ getEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
29
29
  /** Alias kept for existing agent/tools that call getGatewayEndpoint. */
30
- getGatewayEndpoint: (endpointId: string) => DiscordAgentEndpoint;
30
+ getGatewayEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
31
31
  }
32
- export declare function registerDiscordAgentEndpoint(endpointId: string, endpoint: DiscordAgentEndpoint): () => void;
32
+ export declare function registerDiscordAgentEndpoint(endpointKey: string, endpoint: DiscordAgentEndpoint): () => void;
33
33
  /** Optional override used by tests / transitional callers. Pass `null` to clear. */
34
34
  export declare function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void;
35
35
  export declare function getDiscordAgentDeps(): DiscordAgentDeps;
@@ -4,11 +4,11 @@
4
4
  */
5
5
  const endpoints = new Map();
6
6
  let override = null;
7
- export function registerDiscordAgentEndpoint(endpointId, endpoint) {
8
- endpoints.set(endpointId, endpoint);
7
+ export function registerDiscordAgentEndpoint(endpointKey, endpoint) {
8
+ endpoints.set(endpointKey, endpoint);
9
9
  return () => {
10
- if (endpoints.get(endpointId) === endpoint) {
11
- endpoints.delete(endpointId);
10
+ if (endpoints.get(endpointKey) === endpoint) {
11
+ endpoints.delete(endpointKey);
12
12
  }
13
13
  };
14
14
  }
@@ -16,10 +16,10 @@ export function registerDiscordAgentEndpoint(endpointId, endpoint) {
16
16
  export function setDiscordAgentDeps(deps) {
17
17
  override = deps;
18
18
  }
19
- function lookup(endpointId) {
20
- const registered = endpoints.get(endpointId);
19
+ function lookup(endpointKey) {
20
+ const registered = endpoints.get(endpointKey);
21
21
  if (!registered)
22
- throw new Error(`Endpoint ${endpointId} 不存在`);
22
+ throw new Error(`Endpoint ${endpointKey} 不存在`);
23
23
  return registered;
24
24
  }
25
25
  export function getDiscordAgentDeps() {
package/lib/endpoint.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare class DiscordGatewayEndpoint implements EndpointInstance {
20
20
  open(): void;
21
21
  close(): void;
22
22
  stop(): Promise<void>;
23
- send({ target, conversation, payload }: EndpointSendRequest): Promise<string>;
23
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
24
24
  recallMessage(messageId: string): Promise<void>;
25
25
  /** Test / internal: admit a message when open. */
26
26
  admit(msg: DiscordInboundMessage): void;
@@ -64,7 +64,7 @@ export declare class DiscordInteractionsEndpoint implements EndpointInstance {
64
64
  open(): void;
65
65
  close(): void;
66
66
  stop(): Promise<void>;
67
- send({ target, conversation, payload }: EndpointSendRequest): Promise<string>;
67
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
68
68
  recallMessage(messageId: string): Promise<void>;
69
69
  admit(msg: DiscordInboundMessage): void;
70
70
  }
package/lib/endpoint.js CHANGED
@@ -2,17 +2,17 @@
2
2
  * DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
3
3
  */
4
4
  import { ChannelType } from 'discord.js';
5
- import { formatLegacyMessageReference, nativeConversationId, parseLegacyMessageReference, } from '@zhin.js/im-contract';
6
- import { formatCompact, getLogger } from '@zhin.js/logger';
5
+ import { formatLegacyConversationRef, formatLegacyMessageRef, nativeConversationId, parseLegacyMessageReference, } from '@zhin.js/im-contract';
6
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
7
  import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
8
8
  import { connectDiscordGatewayClient, defaultCreateClient, DEFAULT_INTENTS, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
9
- import { formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
9
+ import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
10
10
  import { registerDiscordInteractionRoutes } from './webhook.js';
11
11
  const DISCORD_API = 'https://discord.com/api/v10';
12
12
  /** 出站 HTTP 调用统一 30s 超时。 */
13
13
  const OUTBOUND_TIMEOUT_MS = 30_000;
14
- const logger = getLogger('discord');
15
14
  export class DiscordGatewayEndpoint {
15
+ #logger;
16
16
  #options;
17
17
  #createClient;
18
18
  #client = null;
@@ -37,6 +37,7 @@ export class DiscordGatewayEndpoint {
37
37
  },
38
38
  });
39
39
  constructor(options) {
40
+ this.#logger = getAdapterLogger('discord', options.config.id);
40
41
  this.#options = options;
41
42
  this.#createClient = options.createClient ?? defaultCreateClient;
42
43
  }
@@ -45,7 +46,7 @@ export class DiscordGatewayEndpoint {
45
46
  return;
46
47
  this.#started = true;
47
48
  try {
48
- this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.name, this);
49
+ this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.id, this);
49
50
  const intents = this.#options.config.intents?.length
50
51
  ? [...this.#options.config.intents]
51
52
  : DEFAULT_INTENTS;
@@ -54,16 +55,16 @@ export class DiscordGatewayEndpoint {
54
55
  onMessage: (msg) => this.admit(msg),
55
56
  onButton: (interaction) => this.admitButton(interaction),
56
57
  });
57
- logger.info(formatCompact({
58
+ this.#logger.info(formatCompact({
58
59
  op: 'connect',
59
- endpoint: this.#options.config.name,
60
+ endpoint: this.#options.config.id,
60
61
  mode: 'gateway',
61
62
  user: this.#client.user?.tag,
62
63
  }));
63
64
  }
64
65
  catch (error) {
65
66
  await this.stop();
66
- logger.error('Failed to connect Discord gateway:', error);
67
+ this.#logger.error('Failed to connect Discord gateway:', error);
67
68
  throw error;
68
69
  }
69
70
  }
@@ -88,17 +89,17 @@ export class DiscordGatewayEndpoint {
88
89
  this.#client = null;
89
90
  }
90
91
  this.#started = false;
91
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
92
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
92
93
  }
93
- async send({ target, conversation, payload }) {
94
+ async send({ conversation, payload }) {
94
95
  const body = formatOutboundBody(payload);
95
- const channelId = nativeConversationId(target, conversation);
96
- const snowflake = await this.#sendBody(channelId, body);
97
- const messageId = formatLegacyMessageReference({ target, messageId: snowflake });
98
- logger.debug(formatCompact({
96
+ // recall 链路仍是本端点编码的 legacy 引用,在边界内部从 conversation 派生
97
+ const snowflake = await this.#sendBody(conversation.id, body);
98
+ const messageId = formatLegacyMessageRef({ conversation, id: snowflake });
99
+ this.#logger.debug(formatCompact({
99
100
  op: 'discord_send',
100
- endpoint: this.#options.config.name,
101
- target,
101
+ endpoint: this.#options.config.id,
102
+ target: formatLegacyConversationRef(conversation),
102
103
  messageId,
103
104
  }));
104
105
  return messageId;
@@ -123,26 +124,30 @@ export class DiscordGatewayEndpoint {
123
124
  return;
124
125
  if (msg.authorBot)
125
126
  return;
127
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
126
128
  void this.#options.gateway.receive({
127
- adapter: this.#options.id,
128
- target: msg.channelId,
129
+ conversation,
130
+ message: { conversation, id: msg.id },
129
131
  content: formatInboundContent(msg),
130
132
  segments: formatInboundSegments(msg),
131
- sender: senderDisplayName(msg),
132
- id: msg.id,
133
+ sender: {
134
+ id: msg.authorId,
135
+ name: senderDisplayName(msg) || undefined,
136
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
137
+ },
138
+ endpointId: this.#options.config.id,
139
+ ...(msg.mentionedBot ? { mentioned: true } : {}),
133
140
  metadata: Object.freeze({
134
- endpoint: this.#options.config.name,
135
141
  channelKind: msg.channelKind,
136
142
  userId: msg.authorId,
137
143
  guildId: msg.guildId,
138
144
  permissions: msg.permissionTokens,
139
145
  role: resolveSenderRole(msg),
140
- ...(msg.mentionedBot ? { mentioned: true } : {}),
141
146
  }),
142
147
  }).catch((err) => {
143
- logger.warn(formatCompact({
148
+ this.#logger.warn(formatCompact({
144
149
  op: 'discord_gateway_receive_failed',
145
- target: msg.channelId,
150
+ target: `${conversation.kind}:${conversation.id}`,
146
151
  error: err instanceof Error ? err.message : String(err),
147
152
  }));
148
153
  });
@@ -151,23 +156,23 @@ export class DiscordGatewayEndpoint {
151
156
  admitButton(interaction) {
152
157
  if (!this.#open)
153
158
  return;
159
+ const conversation = discordInboundConversation(String(this.#options.id), interaction);
154
160
  void this.#options.gateway.receive({
155
- adapter: this.#options.id,
156
- target: interaction.channelId,
161
+ conversation,
162
+ message: { conversation, id: interaction.id },
157
163
  content: formatButtonContent(interaction),
158
164
  segments: formatButtonSegments(interaction),
159
- sender: interaction.userName,
160
- id: interaction.id,
165
+ sender: { id: interaction.userId, name: interaction.userName },
166
+ endpointId: this.#options.config.id,
161
167
  metadata: Object.freeze({
162
- endpoint: this.#options.config.name,
163
168
  eventType: 'button',
164
169
  payload: interaction.customId,
165
170
  sourceMessageId: interaction.sourceMessageId,
166
171
  }),
167
172
  }).catch((err) => {
168
- logger.warn(formatCompact({
173
+ this.#logger.warn(formatCompact({
169
174
  op: 'discord_gateway_receive_failed',
170
- target: interaction.channelId,
175
+ target: `${conversation.kind}:${conversation.id}`,
171
176
  error: err instanceof Error ? err.message : String(err),
172
177
  }));
173
178
  });
@@ -306,6 +311,7 @@ export class DiscordGatewayEndpoint {
306
311
  }
307
312
  }
308
313
  export class DiscordInteractionsEndpoint {
314
+ #logger;
309
315
  #options;
310
316
  #fetch;
311
317
  #routeReleases = [];
@@ -315,6 +321,7 @@ export class DiscordInteractionsEndpoint {
315
321
  recall: (messageId) => this.recallMessage(messageId),
316
322
  });
317
323
  constructor(options) {
324
+ this.#logger = getAdapterLogger('discord', options.config.id);
318
325
  this.#options = options;
319
326
  this.#fetch = options.fetch ?? globalThis.fetch;
320
327
  }
@@ -329,9 +336,9 @@ export class DiscordInteractionsEndpoint {
329
336
  return;
330
337
  this.#started = true;
331
338
  this.#routeReleases.push(...registerDiscordInteractionRoutes(this.#options.http, this));
332
- logger.info(formatCompact({
339
+ this.#logger.info(formatCompact({
333
340
  op: 'connect',
334
- endpoint: this.#options.config.name,
341
+ endpoint: this.#options.config.id,
335
342
  mode: 'interactions',
336
343
  path: this.#options.config.interactionsPath,
337
344
  }));
@@ -347,11 +354,11 @@ export class DiscordInteractionsEndpoint {
347
354
  for (const release of this.#routeReleases.splice(0))
348
355
  release();
349
356
  this.#started = false;
350
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
357
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
351
358
  }
352
- async send({ target, conversation, payload }) {
359
+ async send({ conversation, payload }) {
353
360
  const body = formatOutboundBody(payload);
354
- const channelId = nativeConversationId(target, conversation);
361
+ const channelId = conversation.id;
355
362
  const response = await this.#fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
356
363
  method: 'POST',
357
364
  headers: {
@@ -367,7 +374,8 @@ export class DiscordInteractionsEndpoint {
367
374
  }
368
375
  const data = JSON.parse(text);
369
376
  const snowflake = data.id ?? '';
370
- return snowflake ? formatLegacyMessageReference({ target, messageId: snowflake }) : '';
377
+ // recall 链路仍是本端点编码的 legacy 引用,在边界内部从 conversation 派生
378
+ return snowflake ? formatLegacyMessageRef({ conversation, id: snowflake }) : '';
371
379
  }
372
380
  async recallMessage(messageId) {
373
381
  if (!messageId)
@@ -390,24 +398,28 @@ export class DiscordInteractionsEndpoint {
390
398
  admit(msg) {
391
399
  if (!this.#open)
392
400
  return;
401
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
393
402
  void this.#options.gateway.receive({
394
- adapter: this.#options.id,
395
- target: msg.channelId,
403
+ conversation,
404
+ message: { conversation, id: msg.id },
396
405
  content: formatInboundContent(msg),
397
406
  segments: formatInboundSegments(msg),
398
- sender: senderDisplayName(msg),
399
- id: msg.id,
407
+ sender: {
408
+ id: msg.authorId,
409
+ name: senderDisplayName(msg) || undefined,
410
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)] } : {}),
411
+ },
412
+ endpointId: this.#options.config.id,
400
413
  metadata: Object.freeze({
401
- endpoint: this.#options.config.name,
402
414
  channelKind: msg.channelKind,
403
415
  userId: msg.authorId,
404
416
  guildId: msg.guildId,
405
417
  eventType: 'application_command',
406
418
  }),
407
419
  }).catch((err) => {
408
- logger.warn(formatCompact({
420
+ this.#logger.warn(formatCompact({
409
421
  op: 'discord_gateway_receive_failed',
410
- target: msg.channelId,
422
+ target: `${conversation.kind}:${conversation.id}`,
411
423
  error: err instanceof Error ? err.message : String(err),
412
424
  }));
413
425
  });
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, type DiscordAdapterConfig, type DiscordButtonInbound, type DiscordInboundAttachment, type DiscordInboundMessage, type DiscordOutboundBody, type DiscordWireSegment, type ResolvedDiscordConfig, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig, } from './protocol.js';
2
2
  export { getDiscordAgentDeps, registerDiscordAgentEndpoint, setDiscordAgentDeps, type DiscordAgentDeps, type DiscordAgentEndpoint, } from './discord-agent-deps.js';
3
- export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, registerDiscordPlatformPermitChecker, } from './platform-permit.js';
3
+ export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
4
4
  export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type CreateDiscordClient, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
5
5
  export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
6
6
  export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, type DiscordInteractionsHandler, } from './webhook.js';
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, } from './protocol.js';
2
2
  export { getDiscordAgentDeps, registerDiscordAgentEndpoint, setDiscordAgentDeps, } from './discord-agent-deps.js';
3
- export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, registerDiscordPlatformPermitChecker, } from './platform-permit.js';
3
+ export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
4
4
  export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from './endpoint.js';
5
5
  export { connectDiscordGatewayClient, defaultCreateClient, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
6
6
  export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, } from './webhook.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Discord platform permit — Guild 权限位
3
3
  */
4
- import { type Message } from '@zhin.js/core';
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
5
  export declare function platformPermit(perm: string): string;
6
6
  export declare function discordGroupPermitResolver(logicalPerm: string): string;
7
7
  export declare function normalizeDiscordSenderForPermit(input: {
@@ -11,5 +11,4 @@ export declare function normalizeDiscordSenderForPermit(input: {
11
11
  role?: string;
12
12
  permissions?: string[];
13
13
  };
14
- export declare function checkDiscordPlatformPermit(perm: string, message: Message<any>): boolean;
15
- export declare function registerDiscordPlatformPermitChecker(): () => void;
14
+ export declare function checkDiscordPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -1,7 +1,3 @@
1
- /**
2
- * Discord platform permit — Guild 权限位
3
- */
4
- import { registerPlatformPermitChecker } from '@zhin.js/core';
5
1
  const ADAPTER = 'discord';
6
2
  export function platformPermit(perm) {
7
3
  return `platform(${ADAPTER},${perm})`;
@@ -27,12 +23,9 @@ export function normalizeDiscordSenderForPermit(input) {
27
23
  }
28
24
  return { role: 'member', permissions };
29
25
  }
30
- function senderPermits(message) {
31
- const sender = message.$sender;
32
- return { role: sender.role, permissions: sender.permissions ?? [] };
33
- }
34
- export function checkDiscordPlatformPermit(perm, message) {
35
- const { role, permissions } = senderPermits(message);
26
+ export function checkDiscordPlatformPermit(perm, subject) {
27
+ const role = subject.sender?.role?.[0];
28
+ const permissions = subject.sender?.permissions ?? [];
36
29
  const has = (t) => permissions.includes(t);
37
30
  switch (perm) {
38
31
  case 'guild_owner':
@@ -47,6 +40,3 @@ export function checkDiscordPlatformPermit(perm, message) {
47
40
  return false;
48
41
  }
49
42
  }
50
- export function registerDiscordPlatformPermitChecker() {
51
- return registerPlatformPermitChecker(ADAPTER, checkDiscordPlatformPermit);
52
- }
package/lib/protocol.d.ts CHANGED
@@ -3,9 +3,10 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
5
  import type { Segment } from '@zhin.js/core/runtime';
6
+ import type { ConversationRef } from '@zhin.js/im-contract';
6
7
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
7
8
  export interface DiscordAdapterConfig {
8
- readonly name?: string;
9
+ readonly id?: string;
9
10
  readonly token?: string;
10
11
  /** Default `gateway`. `interactions` uses httpHostToken POST + Ed25519 verify. */
11
12
  readonly connection?: 'gateway' | 'interactions';
@@ -25,7 +26,7 @@ export interface DiscordAdapterConfig {
25
26
  /** Transitional: legacy root `endpoints[]` with `context: discord`. */
26
27
  readonly endpoints?: ReadonlyArray<{
27
28
  readonly context?: string;
28
- readonly name?: string;
29
+ readonly id?: string;
29
30
  readonly token?: string;
30
31
  readonly connection?: 'gateway' | 'interactions';
31
32
  readonly intents?: readonly number[];
@@ -41,7 +42,7 @@ export interface DiscordAdapterConfig {
41
42
  export interface ResolvedDiscordGatewayConfig {
42
43
  readonly context: 'discord';
43
44
  readonly connection: 'gateway';
44
- readonly name: string;
45
+ readonly id: string;
45
46
  readonly token: string;
46
47
  readonly intents?: readonly number[];
47
48
  readonly enableSlashCommands: boolean;
@@ -52,7 +53,7 @@ export interface ResolvedDiscordGatewayConfig {
52
53
  export interface ResolvedDiscordInteractionsConfig {
53
54
  readonly context: 'discord';
54
55
  readonly connection: 'interactions';
55
- readonly name: string;
56
+ readonly id: string;
56
57
  readonly token: string;
57
58
  readonly applicationId: string;
58
59
  readonly publicKey: string;
@@ -122,6 +123,16 @@ export interface DiscordOutboundBody {
122
123
  }
123
124
  export declare function resolveDiscordConfig(config?: DiscordAdapterConfig): ResolvedDiscordConfig;
124
125
  export declare function resolveChannelKind(channelType: number | string | undefined): 'private' | 'group' | 'channel';
126
+ /**
127
+ * 入站归一化 → ConversationRef:DM → 'private';GroupDM → 'group';
128
+ * guild 频道 → 'channel' + guild 容器进 parent(guild 是频道容器,kind 'channel')。
129
+ * endpoint.adapter = owner PluginId(CapabilityId 以 \0 分隔)。
130
+ */
131
+ export declare function discordInboundConversation(endpointKey: string, msg: {
132
+ readonly channelId: string;
133
+ readonly channelKind: 'private' | 'group' | 'channel';
134
+ readonly guildId?: string;
135
+ }): ConversationRef;
125
136
  export declare function senderDisplayName(msg: DiscordInboundMessage): string;
126
137
  /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
127
138
  export declare function formatInboundContent(msg: DiscordInboundMessage): string;
package/lib/protocol.js CHANGED
@@ -15,8 +15,8 @@ export function resolveDiscordConfig(config = {}) {
15
15
  if (!token) {
16
16
  throw new TypeError('Discord adapter requires token (plugins.<key>.token or endpoints with context: discord)');
17
17
  }
18
- const name = (typeof config.name === 'string' && config.name)
19
- || (typeof entry?.name === 'string' && entry.name)
18
+ const id = (typeof config.id === 'string' && config.id)
19
+ || (typeof entry?.id === 'string' && entry.id)
20
20
  || process.env.DISCORD_BOT_NAME
21
21
  || 'discord-bot';
22
22
  const connection = config.connection
@@ -31,7 +31,7 @@ export function resolveDiscordConfig(config = {}) {
31
31
  return {
32
32
  context: 'discord',
33
33
  connection: 'interactions',
34
- name,
34
+ id,
35
35
  token,
36
36
  applicationId,
37
37
  publicKey,
@@ -41,7 +41,7 @@ export function resolveDiscordConfig(config = {}) {
41
41
  return {
42
42
  context: 'discord',
43
43
  connection: 'gateway',
44
- name,
44
+ id,
45
45
  token,
46
46
  intents: config.intents ?? entry?.intents,
47
47
  enableSlashCommands: config.enableSlashCommands === true
@@ -59,6 +59,21 @@ export function resolveChannelKind(channelType) {
59
59
  return 'group';
60
60
  return 'channel';
61
61
  }
62
+ /**
63
+ * 入站归一化 → ConversationRef:DM → 'private';GroupDM → 'group';
64
+ * guild 频道 → 'channel' + guild 容器进 parent(guild 是频道容器,kind 'channel')。
65
+ * endpoint.adapter = owner PluginId(CapabilityId 以 \0 分隔)。
66
+ */
67
+ export function discordInboundConversation(endpointKey, msg) {
68
+ return {
69
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
70
+ kind: msg.channelKind,
71
+ id: msg.channelId,
72
+ ...(msg.guildId && msg.channelKind === 'channel'
73
+ ? { parent: { kind: 'channel', id: msg.guildId } }
74
+ : {}),
75
+ };
76
+ }
62
77
  export function senderDisplayName(msg) {
63
78
  return msg.authorName || msg.authorId;
64
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-discord",
3
- "version": "7.0.2",
3
+ "version": "7.0.5",
4
4
  "description": "Zhin.js Discord adapter for Plugin Runtime (Gateway WebSocket)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,21 +33,22 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "discord.js": "^14.27.0",
36
- "@zhin.js/adapter": "1.1.4",
37
- "@zhin.js/command": "1.0.6",
38
- "@zhin.js/core": "1.5.1",
39
- "@zhin.js/host-http": "1.0.5",
40
- "@zhin.js/im-contract": "1.0.0",
41
- "@zhin.js/logger": "1.0.75",
42
- "@zhin.js/plugin-runtime": "1.1.2"
36
+ "@zhin.js/adapter": "1.1.7",
37
+ "@zhin.js/command": "1.0.9",
38
+ "@zhin.js/core": "1.5.4",
39
+ "@zhin.js/host-http": "1.0.8",
40
+ "@zhin.js/im-contract": "1.0.3",
41
+ "@zhin.js/logger": "1.0.76",
42
+ "@zhin.js/permission": "1.0.1",
43
+ "@zhin.js/plugin-runtime": "1.1.5"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "zod": "^4.0.0",
46
- "@zhin.js/adapter": "1.1.4",
47
- "@zhin.js/agent": "1.1.2",
48
- "@zhin.js/core": "1.5.1",
49
- "@zhin.js/plugin-runtime": "1.1.2",
50
- "zhin.js": "6.0.1"
47
+ "@zhin.js/adapter": "1.1.7",
48
+ "@zhin.js/agent": "1.1.5",
49
+ "@zhin.js/core": "1.5.4",
50
+ "@zhin.js/plugin-runtime": "1.1.5",
51
+ "zhin.js": "6.0.4"
51
52
  },
52
53
  "peerDependenciesMeta": {
53
54
  "zhin.js": {
@@ -65,9 +66,9 @@
65
66
  "typescript": "^6.0.3",
66
67
  "vitest": "^4.1.10",
67
68
  "zod": "^4.4.3",
68
- "@zhin.js/agent": "1.1.2",
69
- "@zhin.js/host-http": "1.0.5",
70
- "zhin.js": "6.0.1"
69
+ "@zhin.js/agent": "1.1.5",
70
+ "@zhin.js/host-http": "1.0.8",
71
+ "zhin.js": "6.0.4"
71
72
  },
72
73
  "files": [
73
74
  "adapters",
package/plugin.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { createEndpointRuntimeState } from '@zhin.js/adapter';
3
3
  import { definePlugin } from '@zhin.js/plugin-runtime';
4
- import { registerDiscordPlatformPermitChecker } from "./lib/platform-permit.js";
4
+ import { permissionHostToken } from '@zhin.js/permission';
5
+ import { checkDiscordPlatformPermit } from "./lib/platform-permit.js";
5
6
  import { discordRuntimeStateToken } from "./lib/discord-runtime-state.js";
6
7
  export default definePlugin({
7
8
  name: 'discord',
@@ -9,9 +10,10 @@ export default definePlugin({
9
10
  displayName: 'Discord Gateway Adapter',
10
11
  },
11
12
  setup(context) {
12
- // 运行中 endpoint 注册表(discord.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(discordRuntimeStateToken, createEndpointRuntimeState());
14
- // 平台权限门禁:guild_owner / moderate_members 等(agent 工具 platformPermit)
15
- return registerDiscordPlatformPermitChecker();
14
+ if (context.resources.has(permissionHostToken)) {
15
+ const host = context.resources.use(permissionHostToken);
16
+ return host.registerPlatform('discord', checkDiscordPlatformPermit);
17
+ }
16
18
  },
17
19
  });
package/schema.json CHANGED
@@ -71,24 +71,60 @@
71
71
  "default": "/discord/interactions",
72
72
  "description": "POST path on httpHostToken when connection is interactions."
73
73
  },
74
+ "master": {
75
+ "type": [
76
+ "string",
77
+ "number"
78
+ ],
79
+ "description": "框架 master(Discord user snowflake;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
80
+ },
81
+ "trusted": {
82
+ "type": "array",
83
+ "items": {
84
+ "type": [
85
+ "string",
86
+ "number"
87
+ ],
88
+ "description": "Trusted Discord user snowflake"
89
+ },
90
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
91
+ },
74
92
  "endpoints": {
75
93
  "type": "array",
76
- "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
94
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
77
95
  "items": {
78
96
  "type": "object",
79
97
  "additionalProperties": true,
80
98
  "properties": {
81
- "name": {
82
- "type": "string",
83
- "description": "Discord bot name"
99
+ "master": {
100
+ "type": [
101
+ "string",
102
+ "number"
103
+ ],
104
+ "description": "本 endpoint 的框架 master(Discord user snowflake);覆盖顶层 master"
105
+ },
106
+ "trusted": {
107
+ "type": "array",
108
+ "items": {
109
+ "type": [
110
+ "string",
111
+ "number"
112
+ ],
113
+ "description": "Trusted Discord user snowflake"
114
+ },
115
+ "description": "本 endpoint 的 trusted 列表"
84
116
  },
85
117
  "token": {
86
118
  "type": "string",
87
119
  "description": "Discord bot token"
120
+ },
121
+ "id": {
122
+ "type": "string",
123
+ "description": "Discord bot name"
88
124
  }
89
125
  },
90
126
  "required": [
91
- "name",
127
+ "id",
92
128
  "token"
93
129
  ]
94
130
  }
@@ -39,22 +39,22 @@ export interface DiscordAgentEndpoint {
39
39
  }
40
40
 
41
41
  export interface DiscordAgentDeps {
42
- getEndpoint: (endpointId: string) => DiscordAgentEndpoint;
42
+ getEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
43
43
  /** Alias kept for existing agent/tools that call getGatewayEndpoint. */
44
- getGatewayEndpoint: (endpointId: string) => DiscordAgentEndpoint;
44
+ getGatewayEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
45
45
  }
46
46
 
47
47
  const endpoints = new Map<string, DiscordAgentEndpoint>();
48
48
  let override: DiscordAgentDeps | null = null;
49
49
 
50
50
  export function registerDiscordAgentEndpoint(
51
- endpointId: string,
51
+ endpointKey: string,
52
52
  endpoint: DiscordAgentEndpoint,
53
53
  ): () => void {
54
- endpoints.set(endpointId, endpoint);
54
+ endpoints.set(endpointKey, endpoint);
55
55
  return () => {
56
- if (endpoints.get(endpointId) === endpoint) {
57
- endpoints.delete(endpointId);
56
+ if (endpoints.get(endpointKey) === endpoint) {
57
+ endpoints.delete(endpointKey);
58
58
  }
59
59
  };
60
60
  }
@@ -64,9 +64,9 @@ export function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void {
64
64
  override = deps;
65
65
  }
66
66
 
67
- function lookup(endpointId: string): DiscordAgentEndpoint {
68
- const registered = endpoints.get(endpointId);
69
- if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
67
+ function lookup(endpointKey: string): DiscordAgentEndpoint {
68
+ const registered = endpoints.get(endpointKey);
69
+ if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
70
70
  return registered;
71
71
  }
72
72
 
package/src/endpoint.ts CHANGED
@@ -13,11 +13,12 @@ import type {
13
13
  import type { MessageGateway } from '@zhin.js/core/runtime';
14
14
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
15
15
  import {
16
- formatLegacyMessageReference,
16
+ formatLegacyConversationRef,
17
+ formatLegacyMessageRef,
17
18
  nativeConversationId,
18
19
  parseLegacyMessageReference,
19
20
  } from '@zhin.js/im-contract';
20
- import { formatCompact, getLogger } from '@zhin.js/logger';
21
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
21
22
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
22
23
  import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
23
24
  import {
@@ -30,6 +31,7 @@ import {
30
31
  type DiscordClientTransport,
31
32
  } from './gateway.js';
32
33
  import {
34
+ discordInboundConversation,
33
35
  formatButtonContent,
34
36
  formatButtonSegments,
35
37
  formatInboundContent,
@@ -47,8 +49,6 @@ import { registerDiscordInteractionRoutes } from './webhook.js';
47
49
  const DISCORD_API = 'https://discord.com/api/v10';
48
50
  /** 出站 HTTP 调用统一 30s 超时。 */
49
51
  const OUTBOUND_TIMEOUT_MS = 30_000;
50
- const logger = getLogger('discord');
51
-
52
52
  export type {
53
53
  CreateDiscordClient,
54
54
  DiscordClientTransport,
@@ -62,6 +62,8 @@ export interface DiscordEndpointOptions {
62
62
  }
63
63
 
64
64
  export class DiscordGatewayEndpoint implements EndpointInstance {
65
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
66
+
65
67
  readonly #options: DiscordEndpointOptions;
66
68
  readonly #createClient: CreateDiscordClient;
67
69
  #client: DiscordClientTransport | null = null;
@@ -90,6 +92,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
90
92
  });
91
93
 
92
94
  constructor(options: DiscordEndpointOptions) {
95
+ this.#logger = getAdapterLogger('discord', options.config.id);
93
96
  this.#options = options;
94
97
  this.#createClient = options.createClient ?? defaultCreateClient;
95
98
  }
@@ -98,7 +101,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
98
101
  if (this.#started) return;
99
102
  this.#started = true;
100
103
  try {
101
- this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.name, this);
104
+ this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.id, this);
102
105
  const intents = this.#options.config.intents?.length
103
106
  ? [...this.#options.config.intents]
104
107
  : DEFAULT_INTENTS;
@@ -107,15 +110,15 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
107
110
  onMessage: (msg) => this.admit(msg),
108
111
  onButton: (interaction) => this.admitButton(interaction),
109
112
  });
110
- logger.info(formatCompact({
113
+ this.#logger.info(formatCompact({
111
114
  op: 'connect',
112
- endpoint: this.#options.config.name,
115
+ endpoint: this.#options.config.id,
113
116
  mode: 'gateway',
114
117
  user: this.#client.user?.tag,
115
118
  }));
116
119
  } catch (error) {
117
120
  await this.stop();
118
- logger.error('Failed to connect Discord gateway:', error);
121
+ this.#logger.error('Failed to connect Discord gateway:', error);
119
122
  throw error;
120
123
  }
121
124
  }
@@ -142,18 +145,18 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
142
145
  this.#client = null;
143
146
  }
144
147
  this.#started = false;
145
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
148
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
146
149
  }
147
150
 
148
- async send({ target, conversation, payload }: EndpointSendRequest): Promise<string> {
151
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
149
152
  const body = formatOutboundBody(payload);
150
- const channelId = nativeConversationId(target, conversation);
151
- const snowflake = await this.#sendBody(channelId, body);
152
- const messageId = formatLegacyMessageReference({ target, messageId: snowflake });
153
- logger.debug(formatCompact({
153
+ // recall 链路仍是本端点编码的 legacy 引用,在边界内部从 conversation 派生
154
+ const snowflake = await this.#sendBody(conversation.id, body);
155
+ const messageId = formatLegacyMessageRef({ conversation, id: snowflake });
156
+ this.#logger.debug(formatCompact({
154
157
  op: 'discord_send',
155
- endpoint: this.#options.config.name,
156
- target,
158
+ endpoint: this.#options.config.id,
159
+ target: formatLegacyConversationRef(conversation),
157
160
  messageId,
158
161
  }));
159
162
  return messageId;
@@ -175,26 +178,30 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
175
178
  admit(msg: DiscordInboundMessage): void {
176
179
  if (!this.#open) return;
177
180
  if (msg.authorBot) return;
181
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
178
182
  void this.#options.gateway.receive({
179
- adapter: this.#options.id,
180
- target: msg.channelId,
183
+ conversation,
184
+ message: { conversation, id: msg.id },
181
185
  content: formatInboundContent(msg),
182
186
  segments: formatInboundSegments(msg),
183
- sender: senderDisplayName(msg),
184
- id: msg.id,
187
+ sender: {
188
+ id: msg.authorId,
189
+ name: senderDisplayName(msg) || undefined,
190
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)!] } : {}),
191
+ },
192
+ endpointId: this.#options.config.id,
193
+ ...(msg.mentionedBot ? { mentioned: true } : {}),
185
194
  metadata: Object.freeze({
186
- endpoint: this.#options.config.name,
187
195
  channelKind: msg.channelKind,
188
196
  userId: msg.authorId,
189
197
  guildId: msg.guildId,
190
198
  permissions: msg.permissionTokens,
191
199
  role: resolveSenderRole(msg),
192
- ...(msg.mentionedBot ? { mentioned: true } : {}),
193
200
  }),
194
201
  }).catch((err) => {
195
- logger.warn(formatCompact({
202
+ this.#logger.warn(formatCompact({
196
203
  op: 'discord_gateway_receive_failed',
197
- target: msg.channelId,
204
+ target: `${conversation.kind}:${conversation.id}`,
198
205
  error: err instanceof Error ? err.message : String(err),
199
206
  }));
200
207
  });
@@ -203,23 +210,23 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
203
210
  /** Test / internal: admit a button interaction when open. */
204
211
  admitButton(interaction: DiscordButtonInbound): void {
205
212
  if (!this.#open) return;
213
+ const conversation = discordInboundConversation(String(this.#options.id), interaction);
206
214
  void this.#options.gateway.receive({
207
- adapter: this.#options.id,
208
- target: interaction.channelId,
215
+ conversation,
216
+ message: { conversation, id: interaction.id },
209
217
  content: formatButtonContent(interaction),
210
218
  segments: formatButtonSegments(interaction),
211
- sender: interaction.userName,
212
- id: interaction.id,
219
+ sender: { id: interaction.userId, name: interaction.userName },
220
+ endpointId: this.#options.config.id,
213
221
  metadata: Object.freeze({
214
- endpoint: this.#options.config.name,
215
222
  eventType: 'button',
216
223
  payload: interaction.customId,
217
224
  sourceMessageId: interaction.sourceMessageId,
218
225
  }),
219
226
  }).catch((err) => {
220
- logger.warn(formatCompact({
227
+ this.#logger.warn(formatCompact({
221
228
  op: 'discord_gateway_receive_failed',
222
- target: interaction.channelId,
229
+ target: `${conversation.kind}:${conversation.id}`,
223
230
  error: err instanceof Error ? err.message : String(err),
224
231
  }));
225
232
  });
@@ -416,6 +423,8 @@ export interface DiscordInteractionsEndpointOptions {
416
423
  }
417
424
 
418
425
  export class DiscordInteractionsEndpoint implements EndpointInstance {
426
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
427
+
419
428
  readonly #options: DiscordInteractionsEndpointOptions;
420
429
  readonly #fetch: typeof globalThis.fetch;
421
430
  #routeReleases: HttpRouteRegistration[] = [];
@@ -426,6 +435,7 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
426
435
  });
427
436
 
428
437
  constructor(options: DiscordInteractionsEndpointOptions) {
438
+ this.#logger = getAdapterLogger('discord', options.config.id);
429
439
  this.#options = options;
430
440
  this.#fetch = options.fetch ?? globalThis.fetch;
431
441
  }
@@ -442,9 +452,9 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
442
452
  if (this.#started) return;
443
453
  this.#started = true;
444
454
  this.#routeReleases.push(...registerDiscordInteractionRoutes(this.#options.http, this));
445
- logger.info(formatCompact({
455
+ this.#logger.info(formatCompact({
446
456
  op: 'connect',
447
- endpoint: this.#options.config.name,
457
+ endpoint: this.#options.config.id,
448
458
  mode: 'interactions',
449
459
  path: this.#options.config.interactionsPath,
450
460
  }));
@@ -462,12 +472,12 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
462
472
  this.#open = false;
463
473
  for (const release of this.#routeReleases.splice(0)) release();
464
474
  this.#started = false;
465
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
475
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
466
476
  }
467
477
 
468
- async send({ target, conversation, payload }: EndpointSendRequest): Promise<string> {
478
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
469
479
  const body = formatOutboundBody(payload);
470
- const channelId = nativeConversationId(target, conversation);
480
+ const channelId = conversation.id;
471
481
  const response = await this.#fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
472
482
  method: 'POST',
473
483
  headers: {
@@ -483,7 +493,8 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
483
493
  }
484
494
  const data = JSON.parse(text) as { id?: string };
485
495
  const snowflake = data.id ?? '';
486
- return snowflake ? formatLegacyMessageReference({ target, messageId: snowflake }) : '';
496
+ // recall 链路仍是本端点编码的 legacy 引用,在边界内部从 conversation 派生
497
+ return snowflake ? formatLegacyMessageRef({ conversation, id: snowflake }) : '';
487
498
  }
488
499
 
489
500
  async recallMessage(messageId: string): Promise<void> {
@@ -505,24 +516,28 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
505
516
 
506
517
  admit(msg: DiscordInboundMessage): void {
507
518
  if (!this.#open) return;
519
+ const conversation = discordInboundConversation(String(this.#options.id), msg);
508
520
  void this.#options.gateway.receive({
509
- adapter: this.#options.id,
510
- target: msg.channelId,
521
+ conversation,
522
+ message: { conversation, id: msg.id },
511
523
  content: formatInboundContent(msg),
512
524
  segments: formatInboundSegments(msg),
513
- sender: senderDisplayName(msg),
514
- id: msg.id,
525
+ sender: {
526
+ id: msg.authorId,
527
+ name: senderDisplayName(msg) || undefined,
528
+ ...(resolveSenderRole(msg) ? { roles: [resolveSenderRole(msg)!] } : {}),
529
+ },
530
+ endpointId: this.#options.config.id,
515
531
  metadata: Object.freeze({
516
- endpoint: this.#options.config.name,
517
532
  channelKind: msg.channelKind,
518
533
  userId: msg.authorId,
519
534
  guildId: msg.guildId,
520
535
  eventType: 'application_command',
521
536
  }),
522
537
  }).catch((err) => {
523
- logger.warn(formatCompact({
538
+ this.#logger.warn(formatCompact({
524
539
  op: 'discord_gateway_receive_failed',
525
- target: msg.channelId,
540
+ target: `${conversation.kind}:${conversation.id}`,
526
541
  error: err instanceof Error ? err.message : String(err),
527
542
  }));
528
543
  });
package/src/index.ts CHANGED
@@ -30,7 +30,6 @@ export {
30
30
  discordGroupPermitResolver,
31
31
  normalizeDiscordSenderForPermit,
32
32
  platformPermit,
33
- registerDiscordPlatformPermitChecker,
34
33
  } from './platform-permit.js';
35
34
 
36
35
  export {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Discord platform permit — Guild 权限位
3
3
  */
4
- import { registerPlatformPermitChecker, type Message } from '@zhin.js/core';
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
5
 
6
6
  const ADAPTER = 'discord';
7
7
 
@@ -36,13 +36,9 @@ export function normalizeDiscordSenderForPermit(input: {
36
36
  return { role: 'member', permissions };
37
37
  }
38
38
 
39
- function senderPermits(message: Message<any>): { role?: string; permissions: string[] } {
40
- const sender = message.$sender as { role?: string; permissions?: string[] };
41
- return { role: sender.role, permissions: sender.permissions ?? [] };
42
- }
43
-
44
- export function checkDiscordPlatformPermit(perm: string, message: Message<any>): boolean {
45
- const { role, permissions } = senderPermits(message);
39
+ export function checkDiscordPlatformPermit(perm: string, subject: PermissionSubject): boolean {
40
+ const role = subject.sender?.role?.[0];
41
+ const permissions = subject.sender?.permissions ?? [];
46
42
  const has = (t: string) => permissions.includes(t);
47
43
 
48
44
  switch (perm) {
@@ -59,6 +55,3 @@ export function checkDiscordPlatformPermit(perm: string, message: Message<any>):
59
55
  }
60
56
  }
61
57
 
62
- export function registerDiscordPlatformPermitChecker(): () => void {
63
- return registerPlatformPermitChecker(ADAPTER, checkDiscordPlatformPermit);
64
- }
package/src/protocol.ts CHANGED
@@ -6,13 +6,14 @@
6
6
  import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
7
7
  import { isMediaRef } from '@zhin.js/core';
8
8
  import type { Segment } from '@zhin.js/core/runtime';
9
+ import type { ConversationRef } from '@zhin.js/im-contract';
9
10
  import { formatCompact, getLogger } from '@zhin.js/logger';
10
11
 
11
12
  const logger = getLogger('discord');
12
13
 
13
14
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
14
15
  export interface DiscordAdapterConfig {
15
- readonly name?: string;
16
+ readonly id?: string;
16
17
  readonly token?: string;
17
18
  /** Default `gateway`. `interactions` uses httpHostToken POST + Ed25519 verify. */
18
19
  readonly connection?: 'gateway' | 'interactions';
@@ -32,7 +33,7 @@ export interface DiscordAdapterConfig {
32
33
  /** Transitional: legacy root `endpoints[]` with `context: discord`. */
33
34
  readonly endpoints?: ReadonlyArray<{
34
35
  readonly context?: string;
35
- readonly name?: string;
36
+ readonly id?: string;
36
37
  readonly token?: string;
37
38
  readonly connection?: 'gateway' | 'interactions';
38
39
  readonly intents?: readonly number[];
@@ -49,7 +50,7 @@ export interface DiscordAdapterConfig {
49
50
  export interface ResolvedDiscordGatewayConfig {
50
51
  readonly context: 'discord';
51
52
  readonly connection: 'gateway';
52
- readonly name: string;
53
+ readonly id: string;
53
54
  readonly token: string;
54
55
  readonly intents?: readonly number[];
55
56
  readonly enableSlashCommands: boolean;
@@ -61,7 +62,7 @@ export interface ResolvedDiscordGatewayConfig {
61
62
  export interface ResolvedDiscordInteractionsConfig {
62
63
  readonly context: 'discord';
63
64
  readonly connection: 'interactions';
64
- readonly name: string;
65
+ readonly id: string;
65
66
  readonly token: string;
66
67
  readonly applicationId: string;
67
68
  readonly publicKey: string;
@@ -151,8 +152,8 @@ export function resolveDiscordConfig(config: DiscordAdapterConfig = {}): Resolve
151
152
  'Discord adapter requires token (plugins.<key>.token or endpoints with context: discord)',
152
153
  );
153
154
  }
154
- const name = (typeof config.name === 'string' && config.name)
155
- || (typeof entry?.name === 'string' && entry.name)
155
+ const id = (typeof config.id === 'string' && config.id)
156
+ || (typeof entry?.id === 'string' && entry.id)
156
157
  || process.env.DISCORD_BOT_NAME
157
158
  || 'discord-bot';
158
159
  const connection = config.connection
@@ -170,7 +171,7 @@ export function resolveDiscordConfig(config: DiscordAdapterConfig = {}): Resolve
170
171
  return {
171
172
  context: 'discord',
172
173
  connection: 'interactions',
173
- name,
174
+ id,
174
175
  token,
175
176
  applicationId,
176
177
  publicKey,
@@ -181,7 +182,7 @@ export function resolveDiscordConfig(config: DiscordAdapterConfig = {}): Resolve
181
182
  return {
182
183
  context: 'discord',
183
184
  connection: 'gateway',
184
- name,
185
+ id,
185
186
  token,
186
187
  intents: config.intents ?? entry?.intents,
187
188
  enableSlashCommands: config.enableSlashCommands === true
@@ -199,6 +200,29 @@ export function resolveChannelKind(channelType: number | string | undefined): 'p
199
200
  return 'channel';
200
201
  }
201
202
 
203
+ /**
204
+ * 入站归一化 → ConversationRef:DM → 'private';GroupDM → 'group';
205
+ * guild 频道 → 'channel' + guild 容器进 parent(guild 是频道容器,kind 'channel')。
206
+ * endpoint.adapter = owner PluginId(CapabilityId 以 \0 分隔)。
207
+ */
208
+ export function discordInboundConversation(
209
+ endpointKey: string,
210
+ msg: {
211
+ readonly channelId: string;
212
+ readonly channelKind: 'private' | 'group' | 'channel';
213
+ readonly guildId?: string;
214
+ },
215
+ ): ConversationRef {
216
+ return {
217
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
218
+ kind: msg.channelKind,
219
+ id: msg.channelId,
220
+ ...(msg.guildId && msg.channelKind === 'channel'
221
+ ? { parent: { kind: 'channel' as const, id: msg.guildId } }
222
+ : {}),
223
+ };
224
+ }
225
+
202
226
  export function senderDisplayName(msg: DiscordInboundMessage): string {
203
227
  return msg.authorName || msg.authorId;
204
228
  }