@zhin.js/adapter-slack 6.0.2 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/command@1.0.7
15
+ - @zhin.js/host-http@1.0.6
16
+ - @zhin.js/agent@1.1.3
17
+ - zhin.js@6.0.2
18
+
3
19
  ## 6.0.2
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -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/lib/endpoint.d.ts CHANGED
@@ -1,4 +1,4 @@
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';
@@ -138,10 +138,7 @@ export declare class SlackEndpoint implements EndpointInstance, SlackWebhookHand
138
138
  open(): void;
139
139
  close(): void;
140
140
  stop(): Promise<void>;
141
- send({ target, payload }: {
142
- readonly target: string;
143
- readonly payload: unknown;
144
- }): Promise<string>;
141
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
145
142
  /** Test / internal: admit a message event when open. */
146
143
  admit(event: SlackMessageEvent | SlackEvent): void;
147
144
  admitInteraction(payload: SlackInteractionPayload): void;
package/lib/endpoint.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { SocketModeClient } from '@slack/socket-mode';
5
5
  import { WebClient } from '@slack/web-api';
6
6
  import { formatCompact, getLogger } from '@zhin.js/logger';
7
- import { formatInboundContent, formatInteractionContent, formatSlashContent, inboundMessageId, resolveSlackChannelType, } from './protocol.js';
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';
@@ -106,10 +106,11 @@ export class SlackEndpoint {
106
106
  this.#started = false;
107
107
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
108
108
  }
109
- async send({ target, payload }) {
109
+ async send({ conversation, payload }) {
110
110
  if (!this.#client)
111
111
  throw new Error('Slack client not connected');
112
- const { channel, threadTs } = parseSendTarget(target);
112
+ const channel = conversation.id;
113
+ const threadTs = conversation.threadId;
113
114
  const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, logger);
114
115
  if (result.ts)
115
116
  this.trackMessageChannel(result.ts, channel);
@@ -127,17 +128,22 @@ export class SlackEndpoint {
127
128
  if (!msg.channel || !msg.ts)
128
129
  return;
129
130
  this.trackMessageChannel(msg.ts, msg.channel);
131
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
132
+ const conversation = slackInboundConversation(String(this.#options.id), {
133
+ channelId: msg.channel,
134
+ channelType: msg.channel_type,
135
+ threadId: threadTs,
136
+ });
130
137
  void this.#options.gateway.receive({
131
- adapter: this.#options.id,
132
- target: msg.channel,
138
+ conversation,
139
+ message: { conversation, id: msg.ts },
133
140
  content: formatInboundContent(msg),
134
141
  sender: msg.user ?? msg.channel,
135
- id: inboundMessageId(msg),
136
142
  metadata: Object.freeze({
137
143
  endpoint: this.#options.config.name,
138
144
  channelType: resolveSlackChannelType(msg),
139
145
  userId: msg.user,
140
- threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
146
+ threadTs,
141
147
  ts: msg.ts,
142
148
  // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
143
149
  ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
@@ -145,7 +151,7 @@ export class SlackEndpoint {
145
151
  }).catch((err) => {
146
152
  logger.warn(formatCompact({
147
153
  op: 'slack_gateway_receive_failed',
148
- target: msg.channel,
154
+ target: `${conversation.kind}:${conversation.id}`,
149
155
  error: err instanceof Error ? err.message : String(err),
150
156
  }));
151
157
  });
@@ -161,12 +167,17 @@ export class SlackEndpoint {
161
167
  if (payload.response_url) {
162
168
  postSlackEphemeral(payload.response_url, '已收到', logger);
163
169
  }
170
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
171
+ const conversation = slackInboundConversation(String(this.#options.id), {
172
+ channelId: channelId || userId,
173
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
174
+ channelType: channelId ? undefined : 'im',
175
+ });
164
176
  void this.#options.gateway.receive({
165
- adapter: this.#options.id,
166
- target: channelId || userId,
177
+ conversation,
178
+ message: { conversation, id: actionTs },
167
179
  content: formatInteractionContent(payload),
168
180
  sender: userId,
169
- id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
170
181
  metadata: Object.freeze({
171
182
  endpoint: this.#options.config.name,
172
183
  eventType: 'block_actions',
@@ -176,7 +187,7 @@ export class SlackEndpoint {
176
187
  }).catch((err) => {
177
188
  logger.warn(formatCompact({
178
189
  op: 'slack_gateway_receive_failed',
179
- target: channelId,
190
+ target: `${conversation.kind}:${conversation.id}`,
180
191
  error: err instanceof Error ? err.message : String(err),
181
192
  }));
182
193
  });
@@ -185,12 +196,14 @@ export class SlackEndpoint {
185
196
  if (!this.#open)
186
197
  return;
187
198
  postSlackEphemeral(cmd.response_url, '处理中…', logger);
199
+ const conversation = slackInboundConversation(String(this.#options.id), {
200
+ channelId: cmd.channel_id,
201
+ });
188
202
  void this.#options.gateway.receive({
189
- adapter: this.#options.id,
190
- target: cmd.channel_id,
203
+ conversation,
204
+ message: { conversation, id: cmd.trigger_id },
191
205
  content: formatSlashContent(cmd),
192
206
  sender: cmd.user_id,
193
- id: cmd.trigger_id,
194
207
  metadata: Object.freeze({
195
208
  endpoint: this.#options.config.name,
196
209
  eventType: 'slash_command',
@@ -199,7 +212,7 @@ export class SlackEndpoint {
199
212
  }).catch((err) => {
200
213
  logger.warn(formatCompact({
201
214
  op: 'slack_gateway_receive_failed',
202
- target: cmd.channel_id,
215
+ target: `${conversation.kind}:${conversation.id}`,
203
216
  error: err instanceof Error ? err.message : String(err),
204
217
  }));
205
218
  });
@@ -352,13 +365,6 @@ export class SlackEndpoint {
352
365
  await this.#socket.start();
353
366
  }
354
367
  }
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
368
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
363
369
  const SLACK_LIST_PAGE_SIZE = 1000;
364
370
  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
5
  export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, registerSlackPlatformPermitChecker, slackGroupPermitResolver, } from './platform-permit.js';
6
- export { formatSlackMessageRef, parseSlackMessageRef, slackMessageTs, } from './slack-message-ref.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
5
  export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, registerSlackPlatformPermitChecker, slackGroupPermitResolver, } from './platform-permit.js';
6
- export { formatSlackMessageRef, parseSlackMessageRef, slackMessageTs, } from './slack-message-ref.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/protocol.d.ts CHANGED
@@ -1,4 +1,5 @@
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
5
  readonly name?: 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(endpointId: 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
@@ -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(endpointId, 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: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
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.
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.3",
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,22 @@
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",
37
+ "@zhin.js/adapter": "1.1.5",
38
+ "@zhin.js/command": "1.0.7",
39
+ "@zhin.js/core": "1.5.2",
40
+ "@zhin.js/host-http": "1.0.6",
41
+ "@zhin.js/im-contract": "1.0.1",
41
42
  "@zhin.js/logger": "1.0.75",
42
- "@zhin.js/plugin-runtime": "1.1.2"
43
+ "@zhin.js/plugin-runtime": "1.1.3"
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.5",
48
+ "@zhin.js/agent": "1.1.3",
49
+ "@zhin.js/core": "1.5.2",
50
+ "@zhin.js/host-http": "1.0.6",
51
+ "@zhin.js/plugin-runtime": "1.1.3",
52
+ "zhin.js": "6.0.2"
52
53
  },
53
54
  "peerDependenciesMeta": {
54
55
  "zhin.js": {
@@ -66,8 +67,8 @@
66
67
  "typescript": "^6.0.3",
67
68
  "vitest": "^4.1.10",
68
69
  "zod": "^4.4.3",
69
- "@zhin.js/agent": "1.1.2",
70
- "zhin.js": "6.0.1"
70
+ "@zhin.js/agent": "1.1.3",
71
+ "zhin.js": "6.0.2"
71
72
  },
72
73
  "files": [
73
74
  "adapters",
package/src/endpoint.ts CHANGED
@@ -8,6 +8,7 @@ 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';
@@ -17,8 +18,8 @@ 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,
@@ -202,9 +203,10 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
202
203
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
203
204
  }
204
205
 
205
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
206
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
206
207
  if (!this.#client) throw new Error('Slack client not connected');
207
- const { channel, threadTs } = parseSendTarget(target);
208
+ const channel = conversation.id;
209
+ const threadTs = conversation.threadId;
208
210
  const result = await sendSlackContent(
209
211
  this.#client,
210
212
  payload,
@@ -224,17 +226,22 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
224
226
  if (!msg.channel || !msg.ts) return;
225
227
 
226
228
  this.trackMessageChannel(msg.ts, msg.channel);
229
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
230
+ const conversation = slackInboundConversation(String(this.#options.id), {
231
+ channelId: msg.channel,
232
+ channelType: msg.channel_type,
233
+ threadId: threadTs,
234
+ });
227
235
  void this.#options.gateway.receive({
228
- adapter: this.#options.id,
229
- target: msg.channel,
236
+ conversation,
237
+ message: { conversation, id: msg.ts },
230
238
  content: formatInboundContent(msg),
231
239
  sender: msg.user ?? msg.channel,
232
- id: inboundMessageId(msg),
233
240
  metadata: Object.freeze({
234
241
  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
246
  // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
240
247
  ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
@@ -242,7 +249,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
242
249
  }).catch((err) => {
243
250
  logger.warn(formatCompact({
244
251
  op: 'slack_gateway_receive_failed',
245
- target: msg.channel,
252
+ target: `${conversation.kind}:${conversation.id}`,
246
253
  error: err instanceof Error ? err.message : String(err),
247
254
  }));
248
255
  });
@@ -257,12 +264,17 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
257
264
  if (payload.response_url) {
258
265
  postSlackEphemeral(payload.response_url, '已收到', logger);
259
266
  }
267
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
268
+ const conversation = slackInboundConversation(String(this.#options.id), {
269
+ channelId: channelId || userId,
270
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
271
+ channelType: channelId ? undefined : 'im',
272
+ });
260
273
  void this.#options.gateway.receive({
261
- adapter: this.#options.id,
262
- target: channelId || userId,
274
+ conversation,
275
+ message: { conversation, id: actionTs },
263
276
  content: formatInteractionContent(payload),
264
277
  sender: userId,
265
- id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
266
278
  metadata: Object.freeze({
267
279
  endpoint: this.#options.config.name,
268
280
  eventType: 'block_actions',
@@ -272,7 +284,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
272
284
  }).catch((err) => {
273
285
  logger.warn(formatCompact({
274
286
  op: 'slack_gateway_receive_failed',
275
- target: channelId,
287
+ target: `${conversation.kind}:${conversation.id}`,
276
288
  error: err instanceof Error ? err.message : String(err),
277
289
  }));
278
290
  });
@@ -281,12 +293,14 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
281
293
  admitSlashCommand(cmd: SlackSlashCommand): void {
282
294
  if (!this.#open) return;
283
295
  postSlackEphemeral(cmd.response_url, '处理中…', logger);
296
+ const conversation = slackInboundConversation(String(this.#options.id), {
297
+ channelId: cmd.channel_id,
298
+ });
284
299
  void this.#options.gateway.receive({
285
- adapter: this.#options.id,
286
- target: cmd.channel_id,
300
+ conversation,
301
+ message: { conversation, id: cmd.trigger_id },
287
302
  content: formatSlashContent(cmd),
288
303
  sender: cmd.user_id,
289
- id: cmd.trigger_id,
290
304
  metadata: Object.freeze({
291
305
  endpoint: this.#options.config.name,
292
306
  eventType: 'slash_command',
@@ -295,7 +309,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
295
309
  }).catch((err) => {
296
310
  logger.warn(formatCompact({
297
311
  op: 'slack_gateway_receive_failed',
298
- target: cmd.channel_id,
312
+ target: `${conversation.kind}:${conversation.id}`,
299
313
  error: err instanceof Error ? err.message : String(err),
300
314
  }));
301
315
  });
@@ -461,14 +475,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
461
475
  }
462
476
  }
463
477
 
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
478
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
473
479
  const SLACK_LIST_PAGE_SIZE = 1000;
474
480
 
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,
@@ -52,12 +52,6 @@ export {
52
52
  slackGroupPermitResolver,
53
53
  } from './platform-permit.js';
54
54
 
55
- export {
56
- formatSlackMessageRef,
57
- parseSlackMessageRef,
58
- slackMessageTs,
59
- } from './slack-message-ref.js';
60
-
61
55
  export { normalizeSlackReactionName } from './slack-reaction.js';
62
56
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
63
57
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
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
 
@@ -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
+ endpointId: 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: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
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.