@zhin.js/adapter-slack 6.0.2 → 6.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
  # Changelog
2
2
 
3
+ ## 6.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
+ ## 6.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
+ ## 6.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
  ## 6.0.2
4
99
 
5
100
  ### Patch Changes
package/README.md CHANGED
@@ -6,7 +6,7 @@ Zhin.js Slack 适配器(Plugin Runtime),优先 Socket Mode,也可经 Run
6
6
 
7
7
  - **Socket Mode**(默认):WebSocket 长连接,无需公网 URL
8
8
  - **HTTP Events API**:`httpHostToken` POST(签名验证),**非** legacy host-router/Koa
9
- - 入站经 `messageGatewayToken`;出站 `send({ target, payload })` → `chat.postMessage` / Block Kit
9
+ - 入站经 `messageGatewayToken`;出站 `send({ conversation, payload })` → `chat.postMessage` / Block Kit
10
10
  - 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
11
11
  - Block Kit 按钮、斜杠命令、消息编辑、表情反应等(见 `agent/tools/`)
12
12
 
@@ -24,8 +24,8 @@ pnpm add @zhin.js/adapter-slack
24
24
  - `@zhin.js/plugin-runtime` — `plugin.ts`(`definePlugin`)
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
26
26
 
27
- 入站:`gateway.receive({ adapter, target: channelId, content: text, sender, metadata })`
28
- 出站:`send({ target, payload })` → Web API(`target` 可为 `channel` `channel:thread_ts`)
27
+ 入站:`gateway.receive({ conversation, message: { conversation, id }, content: text, sender, metadata })`(`conversation` 为 ConversationRef:`channel_type: im` → kind `private`,其余 → kind `group`;线程根 ts 进 `threadId`)
28
+ 出站:`send({ conversation, payload })` → Web API(`conversation.id` channel,`conversation.threadId` thread_ts
29
29
 
30
30
  ### 平台权限(platform permit)
31
31
 
package/adapters/slack.js CHANGED
@@ -21,8 +21,8 @@ export default defineAdapter({
21
21
  create(context) {
22
22
  const config = resolveSlackConfig(context.config);
23
23
  // 注册到插件运行时状态(slack.endpoint list 的"运行中"数据源)
24
- context.use(slackRuntimeStateToken).endpoints.set(config.name, {
25
- name: config.name,
24
+ context.use(slackRuntimeStateToken).endpoints.set(config.id, {
25
+ id: config.id,
26
26
  mode: config.mode,
27
27
  });
28
28
  if (config.mode === 'http') {
package/adapters/slack.ts CHANGED
@@ -26,8 +26,8 @@ export default defineAdapter<SlackAdapterConfig>({
26
26
  create(context) {
27
27
  const config = resolveSlackConfig(context.config);
28
28
  // 注册到插件运行时状态(slack.endpoint list 的"运行中"数据源)
29
- context.use(slackRuntimeStateToken).endpoints.set(config.name, {
30
- name: config.name,
29
+ context.use(slackRuntimeStateToken).endpoints.set(config.id, {
30
+ id: config.id,
31
31
  mode: config.mode,
32
32
  });
33
33
  if (config.mode === 'http') {
@@ -17,6 +17,7 @@ export default defineAgentTool<{
17
17
  const { getEndpoint } = getSlackAgentDeps();
18
18
  const endpoint = getEndpoint(endpoint_id);
19
19
  const user = await endpoint.getUserInfo(user_id);
20
+ if (!user) throw new Error(`Slack 用户不存在: ${user_id}`);
20
21
  return {
21
22
  id: user.id,
22
23
  name: user.name,
package/lib/endpoint.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
5
5
  import { type ResolvedSlackConfig, type SlackEvent, type SlackInteractionPayload, type SlackMessageEvent, type SlackSlashCommand } from './protocol.js';
6
+ import { type SlackUserInfo } from './slack-agent-deps.js';
6
7
  import { type SlackChatClient } from './slack-outbound.js';
7
8
  import { type SlackWebhookHandler } from './webhook.js';
8
9
  export interface SlackSocketLike {
@@ -138,10 +139,7 @@ export declare class SlackEndpoint implements EndpointInstance, SlackWebhookHand
138
139
  open(): void;
139
140
  close(): void;
140
141
  stop(): Promise<void>;
141
- send({ target, payload }: {
142
- readonly target: string;
143
- readonly payload: unknown;
144
- }): Promise<string>;
142
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
145
143
  /** Test / internal: admit a message event when open. */
146
144
  admit(event: SlackMessageEvent | SlackEvent): void;
147
145
  admitInteraction(payload: SlackInteractionPayload): void;
@@ -163,7 +161,7 @@ export declare class SlackEndpoint implements EndpointInstance, SlackWebhookHand
163
161
  renameChannel(channel: string, name: string): Promise<boolean>;
164
162
  getChannelMembers(channel: string): Promise<string[]>;
165
163
  getChannelInfo(channel: string): Promise<unknown>;
166
- getUserInfo(user: string): Promise<unknown>;
164
+ getUserInfo(user: string): Promise<SlackUserInfo | undefined>;
167
165
  addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
168
166
  removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
169
167
  pinMessage(channel: string, timestamp: string): Promise<boolean>;
package/lib/endpoint.js CHANGED
@@ -3,8 +3,8 @@
3
3
  */
4
4
  import { SocketModeClient } from '@slack/socket-mode';
5
5
  import { WebClient } from '@slack/web-api';
6
- import { formatCompact, getLogger } from '@zhin.js/logger';
7
- import { formatInboundContent, formatInteractionContent, formatSlashContent, inboundMessageId, resolveSlackChannelType, } from './protocol.js';
6
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
+ import { formatInboundContent, formatInteractionContent, formatSlashContent, resolveSlackChannelType, slackInboundConversation, } from './protocol.js';
8
8
  import { registerSlackAgentEndpoint } from './slack-agent-deps.js';
9
9
  import { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
10
10
  import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
@@ -12,8 +12,8 @@ import { normalizeSlackReactionName } from './slack-reaction.js';
12
12
  import { postSlackEphemeral } from './slack-response-url.js';
13
13
  import { editSlackContent, sendSlackContent } from './slack-outbound.js';
14
14
  import { registerSlackWebhookRoutes } from './webhook.js';
15
- const logger = getLogger('slack');
16
15
  export class SlackEndpoint {
16
+ #logger;
17
17
  #options;
18
18
  #inboundFilter = createSlackInboundFilterState();
19
19
  #messageChannelMap = new Map();
@@ -26,11 +26,12 @@ export class SlackEndpoint {
26
26
  #unregisterAgent;
27
27
  management = createSlackEndpointManagement(this);
28
28
  constructor(options) {
29
+ this.#logger = getAdapterLogger('slack', options.config.id);
29
30
  this.#options = options;
30
31
  }
31
32
  /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
32
33
  get name() {
33
- return this.#options.config.name;
34
+ return this.#options.config.id;
34
35
  }
35
36
  get client() {
36
37
  return this.#client;
@@ -49,7 +50,7 @@ export class SlackEndpoint {
49
50
  const { config } = this.#options;
50
51
  this.#client = this.#options.createClient?.(config.token)
51
52
  ?? new WebClient(config.token);
52
- this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
53
+ this.#unregisterAgent = registerSlackAgentEndpoint(config.id, this);
53
54
  if (config.mode === 'socket') {
54
55
  await this.#startSocket();
55
56
  }
@@ -59,8 +60,8 @@ export class SlackEndpoint {
59
60
  throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
60
61
  }
61
62
  this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
62
- logger.debug(formatCompact({
63
- endpoint: config.name,
63
+ this.#logger.debug(formatCompact({
64
+ endpoint: config.id,
64
65
  op: 'webhook',
65
66
  path: config.webhookPath,
66
67
  }));
@@ -68,16 +69,12 @@ export class SlackEndpoint {
68
69
  const authTest = await this.#client.auth.test();
69
70
  if (authTest.user_id)
70
71
  this.#botUserId = String(authTest.user_id);
71
- logger.info(formatCompact({
72
- op: 'connect',
73
- endpoint: config.name,
74
- mode: config.mode,
75
- platform_user_id: this.#botUserId,
76
- }));
72
+ this.#logger.info(`connected (${config.mode})`
73
+ + (this.#botUserId ? ` | user: ${this.#botUserId}` : ''));
77
74
  }
78
75
  catch (error) {
79
76
  await this.stop();
80
- logger.error('Failed to connect Slack endpoint:', error);
77
+ this.#logger.error('Failed to connect Slack endpoint:', error);
81
78
  throw error;
82
79
  }
83
80
  }
@@ -104,13 +101,14 @@ export class SlackEndpoint {
104
101
  this.#unregisterAgent = undefined;
105
102
  this.#client = undefined;
106
103
  this.#started = false;
107
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
104
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
108
105
  }
109
- async send({ target, payload }) {
106
+ async send({ conversation, payload }) {
110
107
  if (!this.#client)
111
108
  throw new Error('Slack client not connected');
112
- const { channel, threadTs } = parseSendTarget(target);
113
- const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, logger);
109
+ const channel = conversation.id;
110
+ const threadTs = conversation.threadId;
111
+ const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, this.#logger);
114
112
  if (result.ts)
115
113
  this.trackMessageChannel(result.ts, channel);
116
114
  return formatSlackMessageRef(channel, result.ts || String(Date.now()));
@@ -127,25 +125,29 @@ export class SlackEndpoint {
127
125
  if (!msg.channel || !msg.ts)
128
126
  return;
129
127
  this.trackMessageChannel(msg.ts, msg.channel);
128
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
129
+ const conversation = slackInboundConversation(String(this.#options.id), {
130
+ channelId: msg.channel,
131
+ channelType: msg.channel_type,
132
+ threadId: threadTs,
133
+ });
130
134
  void this.#options.gateway.receive({
131
- adapter: this.#options.id,
132
- target: msg.channel,
135
+ conversation,
136
+ message: { conversation, id: msg.ts },
133
137
  content: formatInboundContent(msg),
134
- sender: msg.user ?? msg.channel,
135
- id: inboundMessageId(msg),
138
+ sender: { id: msg.user ?? msg.channel ?? '' },
139
+ endpointId: this.#options.config.id,
140
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
136
141
  metadata: Object.freeze({
137
- endpoint: this.#options.config.name,
138
142
  channelType: resolveSlackChannelType(msg),
139
143
  userId: msg.user,
140
- threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
144
+ threadTs,
141
145
  ts: msg.ts,
142
- // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
143
- ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
144
146
  }),
145
147
  }).catch((err) => {
146
- logger.warn(formatCompact({
148
+ this.#logger.warn(formatCompact({
147
149
  op: 'slack_gateway_receive_failed',
148
- target: msg.channel,
150
+ target: `${conversation.kind}:${conversation.id}`,
149
151
  error: err instanceof Error ? err.message : String(err),
150
152
  }));
151
153
  });
@@ -159,24 +161,29 @@ export class SlackEndpoint {
159
161
  const userId = payload.user.id;
160
162
  const messageTs = payload.message?.ts ?? '';
161
163
  if (payload.response_url) {
162
- postSlackEphemeral(payload.response_url, '已收到', logger);
164
+ postSlackEphemeral(payload.response_url, '已收到', this.#logger);
163
165
  }
166
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
167
+ const conversation = slackInboundConversation(String(this.#options.id), {
168
+ channelId: channelId || userId,
169
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
170
+ channelType: channelId ? undefined : 'im',
171
+ });
164
172
  void this.#options.gateway.receive({
165
- adapter: this.#options.id,
166
- target: channelId || userId,
173
+ conversation,
174
+ message: { conversation, id: actionTs },
167
175
  content: formatInteractionContent(payload),
168
- sender: userId,
169
- id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
176
+ sender: { id: userId },
177
+ endpointId: this.#options.config.id,
170
178
  metadata: Object.freeze({
171
- endpoint: this.#options.config.name,
172
179
  eventType: 'block_actions',
173
180
  actionId: payload.actions[0]?.action_id,
174
181
  threadTs: messageTs || undefined,
175
182
  }),
176
183
  }).catch((err) => {
177
- logger.warn(formatCompact({
184
+ this.#logger.warn(formatCompact({
178
185
  op: 'slack_gateway_receive_failed',
179
- target: channelId,
186
+ target: `${conversation.kind}:${conversation.id}`,
180
187
  error: err instanceof Error ? err.message : String(err),
181
188
  }));
182
189
  });
@@ -184,22 +191,24 @@ export class SlackEndpoint {
184
191
  admitSlashCommand(cmd) {
185
192
  if (!this.#open)
186
193
  return;
187
- postSlackEphemeral(cmd.response_url, '处理中…', logger);
194
+ postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
195
+ const conversation = slackInboundConversation(String(this.#options.id), {
196
+ channelId: cmd.channel_id,
197
+ });
188
198
  void this.#options.gateway.receive({
189
- adapter: this.#options.id,
190
- target: cmd.channel_id,
199
+ conversation,
200
+ message: { conversation, id: cmd.trigger_id },
191
201
  content: formatSlashContent(cmd),
192
- sender: cmd.user_id,
193
- id: cmd.trigger_id,
202
+ sender: { id: cmd.user_id, name: cmd.user_name },
203
+ endpointId: this.#options.config.id,
194
204
  metadata: Object.freeze({
195
- endpoint: this.#options.config.name,
196
205
  eventType: 'slash_command',
197
206
  command: cmd.command,
198
207
  }),
199
208
  }).catch((err) => {
200
- logger.warn(formatCompact({
209
+ this.#logger.warn(formatCompact({
201
210
  op: 'slack_gateway_receive_failed',
202
- target: cmd.channel_id,
211
+ target: `${conversation.kind}:${conversation.id}`,
203
212
  error: err instanceof Error ? err.message : String(err),
204
213
  }));
205
214
  });
@@ -352,13 +361,6 @@ export class SlackEndpoint {
352
361
  await this.#socket.start();
353
362
  }
354
363
  }
355
- function parseSendTarget(target) {
356
- const parsed = parseSlackMessageRef(target);
357
- if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
358
- return { channel: parsed.channel, threadTs: parsed.ts };
359
- }
360
- return { channel: target };
361
- }
362
364
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
363
365
  const SLACK_LIST_PAGE_SIZE = 1000;
364
366
  function createSlackEndpointManagement(endpoint) {
package/lib/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
- export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, inboundMessageId, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, verifySlackSignature, type ResolvedSlackConfig, type SlackAdapterConfig, type SlackBlockAction, type SlackEvent, type SlackEventEnvelope, type SlackInteractionPayload, type SlackMessageEvent, type SlackSlashCommand, type SlackUrlVerification, type SlackWireSegment, } from './protocol.js';
1
+ export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, slackInboundConversation, verifySlackSignature, type ResolvedSlackConfig, type SlackAdapterConfig, type SlackBlockAction, type SlackEvent, type SlackEventEnvelope, type SlackInteractionPayload, type SlackMessageEvent, type SlackSlashCommand, type SlackUrlVerification, type SlackWireSegment, } from './protocol.js';
2
2
  export { SlackEndpoint, type SlackEndpointOptions, type SlackSocketLike, type SlackWebClientLike, } from './endpoint.js';
3
3
  export { registerSlackWebhookRoutes, handleSlackWebhookRequest, type SlackWebhookHandler, } from './webhook.js';
4
4
  export { getSlackAgentDeps, registerSlackAgentEndpoint, setSlackAgentDeps, type SlackAgentDeps, type SlackAgentEndpoint, } from './slack-agent-deps.js';
5
- export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, registerSlackPlatformPermitChecker, slackGroupPermitResolver, } from './platform-permit.js';
6
- export { formatSlackMessageRef, parseSlackMessageRef, slackMessageTs, } from './slack-message-ref.js';
5
+ export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
7
6
  export { normalizeSlackReactionName } from './slack-reaction.js';
8
7
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
9
8
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
package/lib/index.js CHANGED
@@ -1,9 +1,8 @@
1
- export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, inboundMessageId, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, verifySlackSignature, } from './protocol.js';
1
+ export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, slackInboundConversation, verifySlackSignature, } from './protocol.js';
2
2
  export { SlackEndpoint, } from './endpoint.js';
3
3
  export { registerSlackWebhookRoutes, handleSlackWebhookRequest, } from './webhook.js';
4
4
  export { getSlackAgentDeps, registerSlackAgentEndpoint, setSlackAgentDeps, } from './slack-agent-deps.js';
5
- export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, registerSlackPlatformPermitChecker, slackGroupPermitResolver, } from './platform-permit.js';
6
- export { formatSlackMessageRef, parseSlackMessageRef, slackMessageTs, } from './slack-message-ref.js';
5
+ export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
7
6
  export { normalizeSlackReactionName } from './slack-reaction.js';
8
7
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
9
8
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Slack platform permit — workspace + channel 权限
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 slackGroupPermitResolver(logicalPerm: string): string;
7
7
  export declare function normalizeSlackSenderForPermit(input: {
@@ -12,5 +12,4 @@ export declare function normalizeSlackSenderForPermit(input: {
12
12
  role?: string;
13
13
  permissions?: string[];
14
14
  };
15
- export declare function checkSlackPlatformPermit(perm: string, message: Message<any>): boolean;
16
- export declare function registerSlackPlatformPermitChecker(): () => void;
15
+ export declare function checkSlackPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -1,7 +1,3 @@
1
- /**
2
- * Slack platform permit — workspace + channel 权限
3
- */
4
- import { registerPlatformPermitChecker } from '@zhin.js/core';
5
1
  const ADAPTER = 'slack';
6
2
  export function platformPermit(perm) {
7
3
  return `platform(${ADAPTER},${perm})`;
@@ -29,10 +25,9 @@ export function normalizeSlackSenderForPermit(input) {
29
25
  }
30
26
  return { role: 'member', permissions };
31
27
  }
32
- export function checkSlackPlatformPermit(perm, message) {
33
- const sender = message.$sender;
34
- const permissions = sender.permissions ?? [];
35
- const role = sender.role;
28
+ export function checkSlackPlatformPermit(perm, subject) {
29
+ const role = subject.sender?.role?.[0];
30
+ const permissions = subject.sender?.permissions ?? [];
36
31
  const has = (t) => permissions.includes(t);
37
32
  switch (perm) {
38
33
  case 'workspace_owner':
@@ -46,6 +41,3 @@ export function checkSlackPlatformPermit(perm, message) {
46
41
  return false;
47
42
  }
48
43
  }
49
- export function registerSlackPlatformPermitChecker() {
50
- return registerPlatformPermitChecker(ADAPTER, checkSlackPlatformPermit);
51
- }
package/lib/protocol.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
+ import type { ConversationRef } from '@zhin.js/im-contract';
2
3
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
3
4
  export interface SlackAdapterConfig {
4
- readonly name?: string;
5
+ readonly id?: string;
5
6
  readonly token?: string;
6
7
  readonly signingSecret?: string;
7
8
  readonly appToken?: string;
@@ -21,7 +22,7 @@ export interface SlackAdapterConfig {
21
22
  }
22
23
  export interface ResolvedSlackConfig {
23
24
  readonly context: 'slack';
24
- readonly name: string;
25
+ readonly id: string;
25
26
  readonly token: string;
26
27
  readonly mode: 'socket' | 'http';
27
28
  readonly signingSecret: string;
@@ -115,11 +116,20 @@ export interface SlackWireSegment {
115
116
  export declare function resolveSlackConfig(config?: SlackAdapterConfig): ResolvedSlackConfig;
116
117
  export declare function normalizeWebhookPath(path: string): string;
117
118
  export declare function resolveSlackChannelType(event: Pick<SlackEvent, 'channel_type'>): 'private' | 'group';
119
+ /**
120
+ * 入站归一化 → ConversationRef:channel_type `im` → private,其余(channel/group/mpim)
121
+ * → group;交互/斜杠命令无 channel_type 时按平台 id 前缀推断(D* 为 DM)。Slack 无
122
+ * guild 容器概念(team 为 workspace 级,不进 parent);线程根 ts 进 `threadId`。
123
+ */
124
+ export declare function slackInboundConversation(endpointKey: string, opts: {
125
+ readonly channelId: string;
126
+ readonly channelType?: string;
127
+ readonly threadId?: string;
128
+ }): ConversationRef;
118
129
  /** Build inbound text for MessageGateway.receive. */
119
130
  export declare function formatInboundContent(event: SlackMessageEvent | SlackEvent): string;
120
131
  export declare function formatInteractionContent(payload: SlackInteractionPayload): string;
121
132
  export declare function formatSlashContent(cmd: SlackSlashCommand): string;
122
- export declare function inboundMessageId(event: SlackMessageEvent): string;
123
133
  /**
124
134
  * Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
125
135
  * Segment canonicalization is intentionally not done here.
package/lib/protocol.js CHANGED
@@ -22,8 +22,8 @@ export function resolveSlackConfig(config = {}) {
22
22
  if (!token) {
23
23
  throw new TypeError('Slack adapter requires token (plugins.<key>.token or endpoints with context: slack)');
24
24
  }
25
- const name = (typeof config.name === 'string' && config.name)
26
- || (typeof entry?.name === 'string' && entry.name)
25
+ const id = (typeof config.id === 'string' && config.id)
26
+ || (typeof entry?.id === 'string' && entry.id)
27
27
  || process.env.SLACK_BOT_NAME
28
28
  || 'slack-bot';
29
29
  const socketMode = config.socketMode ?? entry?.socketMode;
@@ -45,7 +45,7 @@ export function resolveSlackConfig(config = {}) {
45
45
  }
46
46
  return {
47
47
  context: 'slack',
48
- name,
48
+ id,
49
49
  token,
50
50
  mode,
51
51
  signingSecret,
@@ -63,6 +63,22 @@ export function normalizeWebhookPath(path) {
63
63
  export function resolveSlackChannelType(event) {
64
64
  return event.channel_type === 'im' ? 'private' : 'group';
65
65
  }
66
+ /**
67
+ * 入站归一化 → ConversationRef:channel_type `im` → private,其余(channel/group/mpim)
68
+ * → group;交互/斜杠命令无 channel_type 时按平台 id 前缀推断(D* 为 DM)。Slack 无
69
+ * guild 容器概念(team 为 workspace 级,不进 parent);线程根 ts 进 `threadId`。
70
+ */
71
+ export function slackInboundConversation(endpointKey, opts) {
72
+ const kind = opts.channelType != null
73
+ ? (opts.channelType === 'im' ? 'private' : 'group')
74
+ : (opts.channelId.startsWith('D') ? 'private' : 'group');
75
+ return {
76
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
77
+ kind,
78
+ id: opts.channelId,
79
+ ...(opts.threadId ? { threadId: opts.threadId } : {}),
80
+ };
81
+ }
66
82
  /** Build inbound text for MessageGateway.receive. */
67
83
  export function formatInboundContent(event) {
68
84
  const text = typeof event.text === 'string' ? event.text : '';
@@ -83,9 +99,6 @@ export function formatInteractionContent(payload) {
83
99
  export function formatSlashContent(cmd) {
84
100
  return `${cmd.command} ${cmd.text}`.trim();
85
101
  }
86
- export function inboundMessageId(event) {
87
- return `${event.channel}:${event.ts}`;
88
- }
89
102
  /**
90
103
  * Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
91
104
  * Segment canonicalization is intentionally not done here.
@@ -12,17 +12,30 @@ export interface SlackAgentEndpoint {
12
12
  renameChannel(channel: string, name: string): Promise<boolean>;
13
13
  getChannelMembers(channel: string): Promise<string[]>;
14
14
  getChannelInfo(channel: string): Promise<unknown>;
15
- getUserInfo(user: string): Promise<unknown>;
15
+ getUserInfo(user: string): Promise<SlackUserInfo | undefined>;
16
16
  addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
17
17
  removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
18
18
  pinMessage(channel: string, timestamp: string): Promise<boolean>;
19
19
  unpinMessage(channel: string, timestamp: string): Promise<boolean>;
20
20
  editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
21
21
  }
22
+ /** Slack users.info 返回的用户对象(SDK 响应字段对 agent 工具的投影)。 */
23
+ export interface SlackUserInfo {
24
+ id?: string;
25
+ name?: string;
26
+ real_name?: string;
27
+ is_admin?: boolean;
28
+ is_bot?: boolean;
29
+ profile?: {
30
+ display_name?: string;
31
+ email?: string;
32
+ status_text?: string;
33
+ };
34
+ }
22
35
  export interface SlackAgentDeps {
23
- getEndpoint: (endpointId: string) => SlackAgentEndpoint;
36
+ getEndpoint: (endpointKey: string) => SlackAgentEndpoint;
24
37
  }
25
- export declare function registerSlackAgentEndpoint(endpointId: string, endpoint: SlackAgentEndpoint): () => void;
38
+ export declare function registerSlackAgentEndpoint(endpointKey: string, endpoint: SlackAgentEndpoint): () => void;
26
39
  /** Optional override used by tests / transitional callers. Pass `null` to clear. */
27
40
  export declare function setSlackAgentDeps(deps: SlackAgentDeps | null): void;
28
41
  export declare function getSlackAgentDeps(): SlackAgentDeps;
@@ -4,11 +4,11 @@
4
4
  */
5
5
  const endpoints = new Map();
6
6
  let override = null;
7
- export function registerSlackAgentEndpoint(endpointId, endpoint) {
8
- endpoints.set(endpointId, endpoint);
7
+ export function registerSlackAgentEndpoint(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
  }
@@ -20,10 +20,10 @@ export function getSlackAgentDeps() {
20
20
  if (override)
21
21
  return override;
22
22
  return {
23
- getEndpoint(endpointId) {
24
- const registered = endpoints.get(endpointId);
23
+ getEndpoint(endpointKey) {
24
+ const registered = endpoints.get(endpointKey);
25
25
  if (!registered)
26
- throw new Error(`Endpoint ${endpointId} 不存在`);
26
+ throw new Error(`Endpoint ${endpointKey} 不存在`);
27
27
  return registered;
28
28
  },
29
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-slack",
3
- "version": "6.0.2",
3
+ "version": "6.0.5",
4
4
  "description": "Zhin.js Slack adapter for Plugin Runtime (Socket Mode / HTTP Events)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,21 +34,23 @@
34
34
  "dependencies": {
35
35
  "@slack/socket-mode": "^2.0.7",
36
36
  "@slack/web-api": "^7.19.0",
37
- "@zhin.js/adapter": "1.1.4",
38
- "@zhin.js/command": "1.0.6",
39
- "@zhin.js/core": "1.5.1",
40
- "@zhin.js/host-http": "1.0.5",
41
- "@zhin.js/logger": "1.0.75",
42
- "@zhin.js/plugin-runtime": "1.1.2"
37
+ "@zhin.js/adapter": "1.1.7",
38
+ "@zhin.js/command": "1.0.9",
39
+ "@zhin.js/core": "1.5.4",
40
+ "@zhin.js/host-http": "1.0.8",
41
+ "@zhin.js/im-contract": "1.0.3",
42
+ "@zhin.js/logger": "1.0.76",
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/host-http": "1.0.5",
50
- "@zhin.js/plugin-runtime": "1.1.2",
51
- "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/host-http": "1.0.8",
51
+ "@zhin.js/permission": "1.0.1",
52
+ "@zhin.js/plugin-runtime": "1.1.5",
53
+ "zhin.js": "6.0.4"
52
54
  },
53
55
  "peerDependenciesMeta": {
54
56
  "zhin.js": {
@@ -66,8 +68,8 @@
66
68
  "typescript": "^6.0.3",
67
69
  "vitest": "^4.1.10",
68
70
  "zod": "^4.4.3",
69
- "@zhin.js/agent": "1.1.2",
70
- "zhin.js": "6.0.1"
71
+ "@zhin.js/agent": "1.1.5",
72
+ "zhin.js": "6.0.4"
71
73
  },
72
74
  "files": [
73
75
  "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 { registerSlackPlatformPermitChecker } from "./lib/platform-permit.js";
4
+ import { permissionHostToken } from '@zhin.js/permission';
5
+ import { checkSlackPlatformPermit } from "./lib/platform-permit.js";
5
6
  import { slackRuntimeStateToken } from "./lib/slack-runtime-state.js";
6
7
  export default definePlugin({
7
8
  name: 'slack',
@@ -9,9 +10,10 @@ export default definePlugin({
9
10
  displayName: 'Slack Adapter',
10
11
  },
11
12
  setup(context) {
12
- // 运行中 endpoint 注册表(slack.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(slackRuntimeStateToken, createEndpointRuntimeState());
14
- // 平台权限门禁:workspace_owner / channel_manager 等(agent 工具 platformPermit)
15
- return registerSlackPlatformPermitChecker();
14
+ if (context.resources.has(permissionHostToken)) {
15
+ const host = context.resources.use(permissionHostToken);
16
+ return host.registerPlatform('slack', checkSlackPlatformPermit);
17
+ }
16
18
  },
17
19
  });
package/schema.json CHANGED
@@ -17,16 +17,48 @@
17
17
  "default": 15000,
18
18
  "description": "Socket Mode client ping timeout (ms)"
19
19
  },
20
+ "master": {
21
+ "type": [
22
+ "string",
23
+ "number"
24
+ ],
25
+ "description": "框架 master(Slack user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
26
+ },
27
+ "trusted": {
28
+ "type": "array",
29
+ "items": {
30
+ "type": [
31
+ "string",
32
+ "number"
33
+ ],
34
+ "description": "Trusted Slack user id"
35
+ },
36
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
37
+ },
20
38
  "endpoints": {
21
39
  "type": "array",
22
- "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
40
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
23
41
  "items": {
24
42
  "type": "object",
25
43
  "additionalProperties": true,
26
44
  "properties": {
27
- "name": {
28
- "type": "string",
29
- "description": "Slack bot name"
45
+ "master": {
46
+ "type": [
47
+ "string",
48
+ "number"
49
+ ],
50
+ "description": "本 endpoint 的框架 master(Slack user id);覆盖顶层 master"
51
+ },
52
+ "trusted": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": [
56
+ "string",
57
+ "number"
58
+ ],
59
+ "description": "Trusted Slack user id"
60
+ },
61
+ "description": "本 endpoint 的 trusted 列表"
30
62
  },
31
63
  "token": {
32
64
  "type": "string",
@@ -39,10 +71,14 @@
39
71
  "appToken": {
40
72
  "type": "string",
41
73
  "description": "App-Level Token (xapp-...) for Socket Mode"
74
+ },
75
+ "id": {
76
+ "type": "string",
77
+ "description": "Slack bot name"
42
78
  }
43
79
  },
44
80
  "required": [
45
- "name",
81
+ "id",
46
82
  "token"
47
83
  ]
48
84
  }
package/src/endpoint.ts CHANGED
@@ -8,17 +8,18 @@ import type {
8
8
  EndpointGroup,
9
9
  EndpointInstance,
10
10
  EndpointManagement,
11
+ EndpointSendRequest,
11
12
  } from '@zhin.js/adapter';
12
13
  import type { MessageGateway } from '@zhin.js/core/runtime';
13
14
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
14
- import { formatCompact, getLogger } from '@zhin.js/logger';
15
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
15
16
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
16
17
  import {
17
18
  formatInboundContent,
18
19
  formatInteractionContent,
19
20
  formatSlashContent,
20
- inboundMessageId,
21
21
  resolveSlackChannelType,
22
+ slackInboundConversation,
22
23
  type ResolvedSlackConfig,
23
24
  type SlackEvent,
24
25
  type SlackEventEnvelope,
@@ -26,7 +27,7 @@ import {
26
27
  type SlackMessageEvent,
27
28
  type SlackSlashCommand,
28
29
  } from './protocol.js';
29
- import { registerSlackAgentEndpoint } from './slack-agent-deps.js';
30
+ import { registerSlackAgentEndpoint, type SlackUserInfo } from './slack-agent-deps.js';
30
31
  import {
31
32
  createSlackInboundFilterState,
32
33
  shouldDropSlackInboundMessage,
@@ -37,8 +38,6 @@ import { postSlackEphemeral } from './slack-response-url.js';
37
38
  import { editSlackContent, sendSlackContent, type SlackChatClient } from './slack-outbound.js';
38
39
  import { registerSlackWebhookRoutes, type SlackWebhookHandler } from './webhook.js';
39
40
 
40
- const logger = getLogger('slack');
41
-
42
41
  export interface SlackSocketLike {
43
42
  on(
44
43
  event: string,
@@ -102,6 +101,8 @@ export interface SlackEndpointOptions {
102
101
  }
103
102
 
104
103
  export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
104
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
105
+
105
106
  readonly #options: SlackEndpointOptions;
106
107
  readonly #inboundFilter = createSlackInboundFilterState();
107
108
  readonly #messageChannelMap = new Map<string, string>();
@@ -115,12 +116,13 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
115
116
  readonly management: EndpointManagement = createSlackEndpointManagement(this);
116
117
 
117
118
  constructor(options: SlackEndpointOptions) {
119
+ this.#logger = getAdapterLogger('slack', options.config.id);
118
120
  this.#options = options;
119
121
  }
120
122
 
121
123
  /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
122
124
  get name(): string {
123
- return this.#options.config.name;
125
+ return this.#options.config.id;
124
126
  }
125
127
 
126
128
  get client(): SlackWebClientLike | undefined {
@@ -143,7 +145,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
143
145
  this.#client = this.#options.createClient?.(config.token)
144
146
  ?? (new WebClient(config.token) as unknown as SlackWebClientLike);
145
147
 
146
- this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
148
+ this.#unregisterAgent = registerSlackAgentEndpoint(config.id, this);
147
149
 
148
150
  if (config.mode === 'socket') {
149
151
  await this.#startSocket();
@@ -153,8 +155,8 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
153
155
  throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
154
156
  }
155
157
  this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
156
- logger.debug(formatCompact({
157
- endpoint: config.name,
158
+ this.#logger.debug(formatCompact({
159
+ endpoint: config.id,
158
160
  op: 'webhook',
159
161
  path: config.webhookPath,
160
162
  }));
@@ -163,15 +165,13 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
163
165
  const authTest = await this.#client.auth.test();
164
166
  if (authTest.user_id) this.#botUserId = String(authTest.user_id);
165
167
 
166
- logger.info(formatCompact({
167
- op: 'connect',
168
- endpoint: config.name,
169
- mode: config.mode,
170
- platform_user_id: this.#botUserId,
171
- }));
168
+ this.#logger.info(
169
+ `connected (${config.mode})`
170
+ + (this.#botUserId ? ` | user: ${this.#botUserId}` : ''),
171
+ );
172
172
  } catch (error) {
173
173
  await this.stop();
174
- logger.error('Failed to connect Slack endpoint:', error);
174
+ this.#logger.error('Failed to connect Slack endpoint:', error);
175
175
  throw error;
176
176
  }
177
177
  }
@@ -199,17 +199,18 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
199
199
  this.#unregisterAgent = undefined;
200
200
  this.#client = undefined;
201
201
  this.#started = false;
202
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
202
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
203
203
  }
204
204
 
205
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
205
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
206
206
  if (!this.#client) throw new Error('Slack client not connected');
207
- const { channel, threadTs } = parseSendTarget(target);
207
+ const channel = conversation.id;
208
+ const threadTs = conversation.threadId;
208
209
  const result = await sendSlackContent(
209
210
  this.#client,
210
211
  payload,
211
212
  { channel, threadTs },
212
- logger,
213
+ this.#logger,
213
214
  );
214
215
  if (result.ts) this.trackMessageChannel(result.ts, channel);
215
216
  return formatSlackMessageRef(channel, result.ts || String(Date.now()));
@@ -224,25 +225,29 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
224
225
  if (!msg.channel || !msg.ts) return;
225
226
 
226
227
  this.trackMessageChannel(msg.ts, msg.channel);
228
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
229
+ const conversation = slackInboundConversation(String(this.#options.id), {
230
+ channelId: msg.channel,
231
+ channelType: msg.channel_type,
232
+ threadId: threadTs,
233
+ });
227
234
  void this.#options.gateway.receive({
228
- adapter: this.#options.id,
229
- target: msg.channel,
235
+ conversation,
236
+ message: { conversation, id: msg.ts },
230
237
  content: formatInboundContent(msg),
231
- sender: msg.user ?? msg.channel,
232
- id: inboundMessageId(msg),
238
+ sender: { id: msg.user ?? msg.channel ?? '' },
239
+ endpointId: this.#options.config.id,
240
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
233
241
  metadata: Object.freeze({
234
- endpoint: this.#options.config.name,
235
242
  channelType: resolveSlackChannelType(msg),
236
243
  userId: msg.user,
237
- threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
244
+ threadTs,
238
245
  ts: msg.ts,
239
- // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
240
- ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
241
246
  }),
242
247
  }).catch((err) => {
243
- logger.warn(formatCompact({
248
+ this.#logger.warn(formatCompact({
244
249
  op: 'slack_gateway_receive_failed',
245
- target: msg.channel,
250
+ target: `${conversation.kind}:${conversation.id}`,
246
251
  error: err instanceof Error ? err.message : String(err),
247
252
  }));
248
253
  });
@@ -255,24 +260,29 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
255
260
  const userId = payload.user.id;
256
261
  const messageTs = payload.message?.ts ?? '';
257
262
  if (payload.response_url) {
258
- postSlackEphemeral(payload.response_url, '已收到', logger);
263
+ postSlackEphemeral(payload.response_url, '已收到', this.#logger);
259
264
  }
265
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
266
+ const conversation = slackInboundConversation(String(this.#options.id), {
267
+ channelId: channelId || userId,
268
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
269
+ channelType: channelId ? undefined : 'im',
270
+ });
260
271
  void this.#options.gateway.receive({
261
- adapter: this.#options.id,
262
- target: channelId || userId,
272
+ conversation,
273
+ message: { conversation, id: actionTs },
263
274
  content: formatInteractionContent(payload),
264
- sender: userId,
265
- id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
275
+ sender: { id: userId },
276
+ endpointId: this.#options.config.id,
266
277
  metadata: Object.freeze({
267
- endpoint: this.#options.config.name,
268
278
  eventType: 'block_actions',
269
279
  actionId: payload.actions[0]?.action_id,
270
280
  threadTs: messageTs || undefined,
271
281
  }),
272
282
  }).catch((err) => {
273
- logger.warn(formatCompact({
283
+ this.#logger.warn(formatCompact({
274
284
  op: 'slack_gateway_receive_failed',
275
- target: channelId,
285
+ target: `${conversation.kind}:${conversation.id}`,
276
286
  error: err instanceof Error ? err.message : String(err),
277
287
  }));
278
288
  });
@@ -280,22 +290,24 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
280
290
 
281
291
  admitSlashCommand(cmd: SlackSlashCommand): void {
282
292
  if (!this.#open) return;
283
- postSlackEphemeral(cmd.response_url, '处理中…', logger);
293
+ postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
294
+ const conversation = slackInboundConversation(String(this.#options.id), {
295
+ channelId: cmd.channel_id,
296
+ });
284
297
  void this.#options.gateway.receive({
285
- adapter: this.#options.id,
286
- target: cmd.channel_id,
298
+ conversation,
299
+ message: { conversation, id: cmd.trigger_id },
287
300
  content: formatSlashContent(cmd),
288
- sender: cmd.user_id,
289
- id: cmd.trigger_id,
301
+ sender: { id: cmd.user_id, name: cmd.user_name },
302
+ endpointId: this.#options.config.id,
290
303
  metadata: Object.freeze({
291
- endpoint: this.#options.config.name,
292
304
  eventType: 'slash_command',
293
305
  command: cmd.command,
294
306
  }),
295
307
  }).catch((err) => {
296
- logger.warn(formatCompact({
308
+ this.#logger.warn(formatCompact({
297
309
  op: 'slack_gateway_receive_failed',
298
- target: cmd.channel_id,
310
+ target: `${conversation.kind}:${conversation.id}`,
299
311
  error: err instanceof Error ? err.message : String(err),
300
312
  }));
301
313
  });
@@ -389,9 +401,9 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
389
401
  return result.channel;
390
402
  }
391
403
 
392
- async getUserInfo(user: string): Promise<unknown> {
404
+ async getUserInfo(user: string): Promise<SlackUserInfo | undefined> {
393
405
  const result = await this.#client!.users.info({ user });
394
- return result.user;
406
+ return result.user as SlackUserInfo | undefined;
395
407
  }
396
408
 
397
409
  async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
@@ -461,14 +473,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
461
473
  }
462
474
  }
463
475
 
464
- function parseSendTarget(target: string): { channel: string; threadTs?: string } {
465
- const parsed = parseSlackMessageRef(target);
466
- if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
467
- return { channel: parsed.channel, threadTs: parsed.ts };
468
- }
469
- return { channel: target };
470
- }
471
-
472
476
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
473
477
  const SLACK_LIST_PAGE_SIZE = 1000;
474
478
 
package/src/index.ts CHANGED
@@ -4,12 +4,12 @@ export {
4
4
  formatOutboundWire,
5
5
  formatSlashContent,
6
6
  headerValue,
7
- inboundMessageId,
8
7
  keyboardToBlockKitBlocks,
9
8
  normalizeWebhookPath,
10
9
  readTextBody,
11
10
  resolveSlackChannelType,
12
11
  resolveSlackConfig,
12
+ slackInboundConversation,
13
13
  verifySlackSignature,
14
14
  type ResolvedSlackConfig,
15
15
  type SlackAdapterConfig,
@@ -48,16 +48,9 @@ export {
48
48
  checkSlackPlatformPermit,
49
49
  normalizeSlackSenderForPermit,
50
50
  platformPermit,
51
- registerSlackPlatformPermitChecker,
52
51
  slackGroupPermitResolver,
53
52
  } from './platform-permit.js';
54
53
 
55
- export {
56
- formatSlackMessageRef,
57
- parseSlackMessageRef,
58
- slackMessageTs,
59
- } from './slack-message-ref.js';
60
-
61
54
  export { normalizeSlackReactionName } from './slack-reaction.js';
62
55
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
63
56
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Slack platform permit — workspace + channel 权限
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 = 'slack';
7
7
 
@@ -39,10 +39,9 @@ export function normalizeSlackSenderForPermit(input: {
39
39
  return { role: 'member', permissions };
40
40
  }
41
41
 
42
- export function checkSlackPlatformPermit(perm: string, message: Message<any>): boolean {
43
- const sender = message.$sender as { role?: string; permissions?: string[] };
44
- const permissions = sender.permissions ?? [];
45
- const role = sender.role;
42
+ export function checkSlackPlatformPermit(perm: string, subject: PermissionSubject): boolean {
43
+ const role = subject.sender?.role?.[0];
44
+ const permissions = subject.sender?.permissions ?? [];
46
45
  const has = (t: string) => permissions.includes(t);
47
46
 
48
47
  switch (perm) {
@@ -58,6 +57,3 @@ export function checkSlackPlatformPermit(perm: string, message: Message<any>): b
58
57
  }
59
58
  }
60
59
 
61
- export function registerSlackPlatformPermitChecker(): () => void {
62
- return registerPlatformPermitChecker(ADAPTER, checkSlackPlatformPermit);
63
- }
package/src/protocol.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { createHmac, timingSafeEqual } from 'node:crypto';
6
6
  import type { IncomingMessage } from 'node:http';
7
7
  import { isMediaRef, type MediaRef } from '@zhin.js/core';
8
+ import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
8
9
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
10
  import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
10
11
 
@@ -20,7 +21,7 @@ const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
20
21
 
21
22
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
22
23
  export interface SlackAdapterConfig {
23
- readonly name?: string;
24
+ readonly id?: string;
24
25
  readonly token?: string;
25
26
  readonly signingSecret?: string;
26
27
  readonly appToken?: string;
@@ -41,7 +42,7 @@ export interface SlackAdapterConfig {
41
42
 
42
43
  export interface ResolvedSlackConfig {
43
44
  readonly context: 'slack';
44
- readonly name: string;
45
+ readonly id: string;
45
46
  readonly token: string;
46
47
  readonly mode: 'socket' | 'http';
47
48
  readonly signingSecret: string;
@@ -138,8 +139,8 @@ export function resolveSlackConfig(config: SlackAdapterConfig = {}): ResolvedSla
138
139
  );
139
140
  }
140
141
 
141
- const name = (typeof config.name === 'string' && config.name)
142
- || (typeof entry?.name === 'string' && entry.name)
142
+ const id = (typeof config.id === 'string' && config.id)
143
+ || (typeof entry?.id === 'string' && entry.id)
143
144
  || process.env.SLACK_BOT_NAME
144
145
  || 'slack-bot';
145
146
 
@@ -169,7 +170,7 @@ export function resolveSlackConfig(config: SlackAdapterConfig = {}): ResolvedSla
169
170
 
170
171
  return {
171
172
  context: 'slack',
172
- name,
173
+ id,
173
174
  token,
174
175
  mode,
175
176
  signingSecret,
@@ -192,6 +193,30 @@ export function resolveSlackChannelType(event: Pick<SlackEvent, 'channel_type'>)
192
193
  return event.channel_type === 'im' ? 'private' : 'group';
193
194
  }
194
195
 
196
+ /**
197
+ * 入站归一化 → ConversationRef:channel_type `im` → private,其余(channel/group/mpim)
198
+ * → group;交互/斜杠命令无 channel_type 时按平台 id 前缀推断(D* 为 DM)。Slack 无
199
+ * guild 容器概念(team 为 workspace 级,不进 parent);线程根 ts 进 `threadId`。
200
+ */
201
+ export function slackInboundConversation(
202
+ endpointKey: string,
203
+ opts: {
204
+ readonly channelId: string;
205
+ readonly channelType?: string;
206
+ readonly threadId?: string;
207
+ },
208
+ ): ConversationRef {
209
+ const kind: ConversationKind = opts.channelType != null
210
+ ? (opts.channelType === 'im' ? 'private' : 'group')
211
+ : (opts.channelId.startsWith('D') ? 'private' : 'group');
212
+ return {
213
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
214
+ kind,
215
+ id: opts.channelId,
216
+ ...(opts.threadId ? { threadId: opts.threadId } : {}),
217
+ };
218
+ }
219
+
195
220
  /** Build inbound text for MessageGateway.receive. */
196
221
  export function formatInboundContent(event: SlackMessageEvent | SlackEvent): string {
197
222
  const text = typeof event.text === 'string' ? event.text : '';
@@ -213,10 +238,6 @@ export function formatSlashContent(cmd: SlackSlashCommand): string {
213
238
  return `${cmd.command} ${cmd.text}`.trim();
214
239
  }
215
240
 
216
- export function inboundMessageId(event: SlackMessageEvent): string {
217
- return `${event.channel}:${event.ts}`;
218
- }
219
-
220
241
  /**
221
242
  * Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
222
243
  * Segment canonicalization is intentionally not done here.
@@ -13,7 +13,7 @@ export interface SlackAgentEndpoint {
13
13
  renameChannel(channel: string, name: string): Promise<boolean>;
14
14
  getChannelMembers(channel: string): Promise<string[]>;
15
15
  getChannelInfo(channel: string): Promise<unknown>;
16
- getUserInfo(user: string): Promise<unknown>;
16
+ getUserInfo(user: string): Promise<SlackUserInfo | undefined>;
17
17
  addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
18
18
  removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
19
19
  pinMessage(channel: string, timestamp: string): Promise<boolean>;
@@ -21,21 +21,35 @@ export interface SlackAgentEndpoint {
21
21
  editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
22
22
  }
23
23
 
24
+ /** Slack users.info 返回的用户对象(SDK 响应字段对 agent 工具的投影)。 */
25
+ export interface SlackUserInfo {
26
+ id?: string;
27
+ name?: string;
28
+ real_name?: string;
29
+ is_admin?: boolean;
30
+ is_bot?: boolean;
31
+ profile?: {
32
+ display_name?: string;
33
+ email?: string;
34
+ status_text?: string;
35
+ };
36
+ }
37
+
24
38
  export interface SlackAgentDeps {
25
- getEndpoint: (endpointId: string) => SlackAgentEndpoint;
39
+ getEndpoint: (endpointKey: string) => SlackAgentEndpoint;
26
40
  }
27
41
 
28
42
  const endpoints = new Map<string, SlackAgentEndpoint>();
29
43
  let override: SlackAgentDeps | null = null;
30
44
 
31
45
  export function registerSlackAgentEndpoint(
32
- endpointId: string,
46
+ endpointKey: string,
33
47
  endpoint: SlackAgentEndpoint,
34
48
  ): () => void {
35
- endpoints.set(endpointId, endpoint);
49
+ endpoints.set(endpointKey, endpoint);
36
50
  return () => {
37
- if (endpoints.get(endpointId) === endpoint) {
38
- endpoints.delete(endpointId);
51
+ if (endpoints.get(endpointKey) === endpoint) {
52
+ endpoints.delete(endpointKey);
39
53
  }
40
54
  };
41
55
  }
@@ -48,9 +62,9 @@ export function setSlackAgentDeps(deps: SlackAgentDeps | null): void {
48
62
  export function getSlackAgentDeps(): SlackAgentDeps {
49
63
  if (override) return override;
50
64
  return {
51
- getEndpoint(endpointId: string): SlackAgentEndpoint {
52
- const registered = endpoints.get(endpointId);
53
- if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
65
+ getEndpoint(endpointKey: string): SlackAgentEndpoint {
66
+ const registered = endpoints.get(endpointKey);
67
+ if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
54
68
  return registered;
55
69
  },
56
70
  };