@zhin.js/adapter-slack 1.1.17 → 1.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +21 -4
  3. package/adapters/slack/index.js +3 -23
  4. package/adapters/slack/index.ts +2 -23
  5. package/agents/slack/skills/slack-messages/tools/edit_message/index.js +3 -3
  6. package/agents/slack/skills/slack-messages/tools/edit_message/index.ts +2 -2
  7. package/commands/slack/endpoint/definition.js +0 -2
  8. package/commands/slack/endpoint/definition.ts +0 -2
  9. package/lib/adapter.d.ts +16 -0
  10. package/lib/adapter.js +54 -0
  11. package/lib/client.d.ts +3 -7
  12. package/lib/client.js +1 -2
  13. package/lib/console-spec.d.ts +12 -0
  14. package/lib/console-spec.js +18 -0
  15. package/lib/console.d.ts +2 -0
  16. package/lib/console.js +13 -0
  17. package/lib/endpoint.d.ts +12 -115
  18. package/lib/endpoint.js +237 -183
  19. package/lib/index.d.ts +2 -1
  20. package/lib/index.js +1 -1
  21. package/lib/protocol.d.ts +8 -2
  22. package/lib/slack-outbound.d.ts +16 -12
  23. package/lib/slack-outbound.js +28 -19
  24. package/lib/web-client.d.ts +1 -0
  25. package/lib/web-client.js +4 -1
  26. package/package.json +25 -17
  27. package/pages/index/index.js +15 -0
  28. package/pages/index/index.ts +16 -0
  29. package/plugin.js +3 -4
  30. package/src/adapter.ts +38 -0
  31. package/src/client.ts +2 -10
  32. package/src/console-spec.ts +23 -0
  33. package/src/console.ts +17 -0
  34. package/src/endpoint.ts +239 -226
  35. package/src/index.ts +1 -1
  36. package/src/protocol.ts +7 -4
  37. package/src/slack-outbound.ts +50 -37
  38. package/src/web-client.ts +5 -1
  39. package/lib/slack-runtime-state.d.ts +0 -1
  40. package/lib/slack-runtime-state.js +0 -6
  41. package/src/slack-runtime-state.ts +0 -7
package/lib/endpoint.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
3
3
  */
4
+ import { WebClient } from '@slack/web-api';
4
5
  import { SocketModeClient } from '@slack/socket-mode';
5
- import { createSlackWebApiAgent } from './web-api-proxy.js';
6
6
  import { createSlackStreamAgent } from './stream-proxy.js';
7
7
  import { slackSocketConnectionError } from './socket-error.js';
8
- import { createSlackWebClient, slackDeliveryError } from './web-client.js';
9
- import { Endpoint, createEndpointLifecycle, } from 'zhin.js/adapter';
8
+ import { slackWebClientOptions, slackDeliveryError } from './web-client.js';
9
+ import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
10
10
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
11
11
  import { formatInboundContent, formatInteractionContent, formatSlashContent, resolveSlackChannelType, slackInboundConversation, } from './protocol.js';
12
12
  import { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
@@ -16,81 +16,79 @@ import { postSlackEphemeral } from './slack-response-url.js';
16
16
  import { editSlackContent, sendSlackContent } from './slack-outbound.js';
17
17
  import { registerSlackWebhookRoutes } from './webhook.js';
18
18
  import { receiveSlackSideEvent } from './side-event-dispatch.js';
19
- export class SlackEndpoint extends Endpoint {
19
+ export class SlackEndpoint extends WebClient {
20
+ [endpointSpec];
20
21
  #logger;
21
22
  #options;
22
23
  #inboundFilter = createSlackInboundFilterState();
23
24
  #messageChannelMap = new Map();
24
- #client;
25
- #webApiAgent;
26
25
  #socket;
27
26
  #lifecycle;
28
27
  #routeReleases = [];
29
28
  #botUserId;
30
- #open = false;
29
+ #executionSignal;
31
30
  #started = false;
32
- management = createSlackEndpointManagement(() => this.client);
33
- control = Object.freeze({
34
- recall: async (message) => {
31
+ #management = createSlackEndpointManagement(() => this);
32
+ #control = Object.freeze({
33
+ recall: async ({ message, signal }) => {
35
34
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
36
- if (!ref || !this.#client)
35
+ if (!ref || !this.#started)
37
36
  return;
38
- await this.#client.chat.delete(ref);
37
+ await createEndpointOperation({ signal }).run(() => this.chat.delete(ref));
39
38
  },
40
- edit: async (message, content) => {
39
+ edit: async ({ message, content, signal }) => {
41
40
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
42
41
  if (!ref)
43
42
  return null;
44
- await this.#editMessage(ref.channel, ref.ts, content);
43
+ await this.#editMessage({ channel: ref.channel, ts: ref.ts, content, signal });
45
44
  return message.id;
46
45
  },
47
- addReaction: async (message, emoji) => {
46
+ addReaction: async ({ message, emoji, signal }) => {
48
47
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
49
48
  if (!ref)
50
49
  return null;
51
50
  const reaction = normalizeSlackReactionName(emoji);
52
- await this.#addReaction(ref.channel, ref.ts, reaction);
51
+ await this.#addReaction({ channel: ref.channel, timestamp: ref.ts, name: reaction, signal });
53
52
  return reaction;
54
53
  },
55
- removeReaction: async (message, reactionId) => {
54
+ removeReaction: async ({ message, reactionId, signal }) => {
56
55
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
57
56
  if (!ref)
58
57
  return;
59
- await this.#removeReaction(ref.channel, ref.ts, reactionId);
58
+ await this.#removeReaction({ channel: ref.channel, timestamp: ref.ts, name: reactionId, signal });
60
59
  },
61
60
  });
62
61
  constructor(options) {
63
- super();
62
+ super(options.config.token, slackWebClientOptions({ ...options.webOptions, ...(options.webApiAgent ? { agent: options.webApiAgent } : {}) }));
64
63
  this.#logger = getAdapterLogger('slack', options.config.id);
65
64
  this.#options = options;
66
65
  this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
66
+ const endpoint = this;
67
+ this[endpointSpec] = createEndpointSpecification(this, {
68
+ start: signal => this.#startFramework(signal), open: () => undefined, close: () => undefined,
69
+ stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
70
+ name: options.config.id, get transportState() { return endpoint.#transportState; },
71
+ management: this.#management, control: this.#control,
72
+ });
67
73
  }
68
74
  /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
69
75
  get name() {
70
76
  return this.#options.config.id;
71
77
  }
72
- get client() {
73
- if (!this.#client)
74
- throw new Error('Slack client not connected');
75
- return this.#client;
76
- }
77
- get transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
78
+ get #transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
78
79
  get platformUserId() {
79
80
  return this.#botUserId;
80
81
  }
81
82
  get config() {
82
83
  return this.#options.config;
83
84
  }
84
- async start() {
85
+ async #startFramework(signal) {
86
+ this.#executionSignal = signal;
85
87
  if (this.#started)
86
88
  return;
87
89
  this.#started = true;
88
90
  try {
89
91
  const { config } = this.#options;
90
- if (config.webApiProxy && !this.#options.createClient)
91
- this.#webApiAgent = createSlackWebApiAgent(config.webApiProxy);
92
- this.#client = this.#options.createClient?.(config.token)
93
- ?? createSlackWebClient(config.token, this.#webApiAgent ? { agent: this.#webApiAgent } : {});
94
92
  if (config.mode === 'socket') {
95
93
  await this.#startSocket();
96
94
  }
@@ -106,28 +104,20 @@ export class SlackEndpoint extends Endpoint {
106
104
  path: config.webhookPath,
107
105
  }));
108
106
  }
109
- const authTest = await this.#client.auth.test();
107
+ const authTest = await this.auth.test();
110
108
  if (authTest.user_id)
111
109
  this.#botUserId = String(authTest.user_id);
112
110
  this.#logger.info(`connected (${config.mode})`
113
111
  + (this.#botUserId ? ` | user: ${this.#botUserId}` : ''));
114
112
  }
115
113
  catch (error) {
116
- await this.stop();
114
+ await this.#stopFramework();
117
115
  this.#logger.error('Failed to connect Slack endpoint:', error);
118
116
  throw error;
119
117
  }
120
118
  }
121
- open() {
122
- this.#open = true;
123
- }
124
- close() {
125
- this.#open = false;
126
- }
127
- async stop() {
128
- this.#open = false;
129
- this.#webApiAgent?.destroy();
130
- this.#webApiAgent = undefined;
119
+ async #stopFramework() {
120
+ this.#options.webApiAgent?.destroy();
131
121
  await this.#lifecycle.stop();
132
122
  if (this.#socket) {
133
123
  try {
@@ -140,17 +130,16 @@ export class SlackEndpoint extends Endpoint {
140
130
  }
141
131
  for (const release of this.#routeReleases.splice(0))
142
132
  release();
143
- this.#client = undefined;
144
133
  this.#started = false;
145
134
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
146
135
  }
147
- async send({ conversation, payload }) {
148
- if (!this.#client)
136
+ async #sendFramework({ conversation, payload, signal }) {
137
+ if (!this.#started)
149
138
  throw new Error('Slack client not connected');
150
139
  const channel = conversation.id;
151
140
  const threadTs = conversation.threadId;
152
141
  try {
153
- const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, this.#logger);
142
+ const result = await sendSlackContent(this, { content: payload, channel, threadTs, logger: this.#logger, signal });
154
143
  this.trackMessageChannel(result.ts, channel);
155
144
  return formatSlackMessageRef(channel, result.ts);
156
145
  }
@@ -159,123 +148,131 @@ export class SlackEndpoint extends Endpoint {
159
148
  }
160
149
  }
161
150
  /** Test / internal: admit a message event when open. */
162
- admit(event) {
163
- if (!this.#open)
164
- return;
165
- void this.#emitPlatformEvent(event.type || 'event', event);
166
- if (event.type !== 'message' && event.type !== 'app_mention')
167
- return;
168
- const msg = event;
169
- if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
170
- return;
171
- if (!msg.channel || !msg.ts)
172
- return;
173
- this.trackMessageChannel(msg.ts, msg.channel);
174
- const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
175
- const conversation = slackInboundConversation(String(this.#options.id), {
176
- channelId: msg.channel,
177
- channelType: msg.channel_type,
178
- threadId: threadTs,
179
- });
180
- void this.emit('message.receive', {
181
- conversation,
182
- message: { conversation, id: msg.ts },
183
- content: formatInboundContent(msg),
184
- sender: { id: msg.user ?? msg.channel ?? '' },
185
- endpointId: this.#options.config.id,
186
- ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
187
- metadata: Object.freeze({
188
- channelType: resolveSlackChannelType(msg),
189
- userId: msg.user,
190
- threadTs,
191
- ts: msg.ts,
192
- }),
193
- }).catch((err) => {
194
- this.#logger.warn(formatCompact({
195
- op: 'slack_gateway_receive_failed',
196
- target: `${conversation.kind}:${conversation.id}`,
197
- error: err instanceof Error ? err.message : String(err),
198
- }));
199
- });
151
+ admit(event, original = event) {
152
+ void this[endpointSpec].events.run(() => {
153
+ void this.#emitPlatformEvent(event.type || 'event', event);
154
+ if (event.type !== 'message' && event.type !== 'app_mention')
155
+ return;
156
+ const msg = event;
157
+ if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
158
+ return;
159
+ if (!msg.channel || !msg.ts)
160
+ return;
161
+ this.trackMessageChannel(msg.ts, msg.channel);
162
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
163
+ const conversation = slackInboundConversation(String(this.#options.id), {
164
+ channelId: msg.channel,
165
+ channelType: msg.channel_type,
166
+ threadId: threadTs,
167
+ });
168
+ void this[endpointSpec].events.emit('message.receive', {
169
+ conversation,
170
+ event: original,
171
+ ...(typeof msg.ts === 'string' && Number.isFinite(Number(msg.ts)) ? { platformSentAt: Number(msg.ts) * 1_000 } : {}),
172
+ message: { conversation, id: msg.ts },
173
+ content: formatInboundContent(msg),
174
+ sender: { id: msg.user ?? msg.channel ?? '' },
175
+ endpointId: this.#options.config.id,
176
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
177
+ metadata: Object.freeze({
178
+ channelType: resolveSlackChannelType(msg),
179
+ userId: msg.user,
180
+ threadTs,
181
+ ts: msg.ts,
182
+ }),
183
+ }).catch((err) => {
184
+ this.#logger.warn(formatCompact({
185
+ op: 'slack_gateway_receive_failed',
186
+ target: `${conversation.kind}:${conversation.id}`,
187
+ error: err instanceof Error ? err.message : String(err),
188
+ }));
189
+ });
190
+ }).catch(() => this.#logger.debug('Inbound callback did not complete'));
200
191
  }
201
192
  admitInteraction(payload) {
202
- if (!this.#open)
203
- return;
204
- void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
205
- if (payload.type !== 'block_actions' || !payload.actions?.length)
206
- return;
207
- const channelId = payload.channel?.id ?? '';
208
- const userId = payload.user.id;
209
- const messageTs = payload.message?.ts ?? '';
210
- const sourceThread = payload.message?.thread_ts;
211
- const threadTs = typeof sourceThread === 'string' && sourceThread && sourceThread !== messageTs ? sourceThread : undefined;
212
- if (payload.response_url) {
213
- postSlackEphemeral(payload.response_url, '已收到', this.#logger);
214
- }
215
- const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
216
- const conversation = slackInboundConversation(String(this.#options.id), {
217
- channelId: channelId || userId,
218
- // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
219
- channelType: channelId ? undefined : 'im',
220
- threadId: threadTs,
221
- });
222
- void this.emit('message.receive', {
223
- conversation,
224
- message: { conversation, id: actionTs },
225
- content: formatInteractionContent(payload),
226
- segments: [{ type: 'action', data: { id: actionTs, payload: payload.actions[0].value ?? payload.actions[0].action_id, ...(messageTs ? { sourceMessageId: messageTs } : {}) } }],
227
- sender: { id: userId },
228
- endpointId: this.#options.config.id,
229
- metadata: Object.freeze({
230
- eventType: 'block_actions',
231
- actionId: payload.actions[0]?.action_id,
232
- threadTs,
233
- }),
234
- }).catch((err) => {
235
- this.#logger.warn(formatCompact({
236
- op: 'slack_gateway_receive_failed',
237
- target: `${conversation.kind}:${conversation.id}`,
238
- error: err instanceof Error ? err.message : String(err),
239
- }));
240
- });
193
+ void this[endpointSpec].events.run(() => {
194
+ void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
195
+ if (payload.type !== 'block_actions' || !payload.actions?.length)
196
+ return;
197
+ const channelId = payload.channel?.id ?? '';
198
+ const userId = payload.user.id;
199
+ const messageTs = payload.message?.ts ?? '';
200
+ const sourceThread = payload.message?.thread_ts;
201
+ const threadTs = typeof sourceThread === 'string' && sourceThread && sourceThread !== messageTs ? sourceThread : undefined;
202
+ if (payload.response_url) {
203
+ postSlackEphemeral(payload.response_url, '已收到', this.#logger);
204
+ }
205
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
206
+ const platformSentAt = Number(payload.actions[0]?.action_ts) * 1_000;
207
+ const conversation = slackInboundConversation(String(this.#options.id), {
208
+ channelId: channelId || userId,
209
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
210
+ channelType: channelId ? undefined : 'im',
211
+ threadId: threadTs,
212
+ });
213
+ void this[endpointSpec].events.emit('message.receive', {
214
+ conversation,
215
+ event: payload,
216
+ ...(Number.isFinite(platformSentAt) ? { platformSentAt } : {}),
217
+ message: { conversation, id: actionTs },
218
+ content: formatInteractionContent(payload),
219
+ segments: [{ type: 'action', data: { id: actionTs, payload: payload.actions[0].value ?? payload.actions[0].action_id, ...(messageTs ? { sourceMessageId: messageTs } : {}) } }],
220
+ sender: { id: userId },
221
+ endpointId: this.#options.config.id,
222
+ metadata: Object.freeze({
223
+ eventType: 'block_actions',
224
+ actionId: payload.actions[0]?.action_id,
225
+ threadTs,
226
+ }),
227
+ }).catch((err) => {
228
+ this.#logger.warn(formatCompact({
229
+ op: 'slack_gateway_receive_failed',
230
+ target: `${conversation.kind}:${conversation.id}`,
231
+ error: err instanceof Error ? err.message : String(err),
232
+ }));
233
+ });
234
+ }).catch(() => this.#logger.debug('Inbound callback did not complete'));
241
235
  }
242
236
  admitSlashCommand(cmd) {
243
- if (!this.#open)
244
- return;
245
- void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
246
- postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
247
- const conversation = slackInboundConversation(String(this.#options.id), {
248
- channelId: cmd.channel_id,
249
- });
250
- void this.emit('message.receive', {
251
- conversation,
252
- message: { conversation, id: cmd.trigger_id },
253
- content: formatSlashContent(cmd),
254
- sender: { id: cmd.user_id, name: cmd.user_name },
255
- endpointId: this.#options.config.id,
256
- metadata: Object.freeze({
257
- eventType: 'slash_command',
258
- command: cmd.command,
259
- }),
260
- }).catch((err) => {
261
- this.#logger.warn(formatCompact({
262
- op: 'slack_gateway_receive_failed',
263
- target: `${conversation.kind}:${conversation.id}`,
264
- error: err instanceof Error ? err.message : String(err),
265
- }));
266
- });
237
+ void this[endpointSpec].events.run(() => {
238
+ void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
239
+ postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
240
+ const conversation = slackInboundConversation(String(this.#options.id), {
241
+ channelId: cmd.channel_id,
242
+ });
243
+ void this[endpointSpec].events.emit('message.receive', {
244
+ conversation,
245
+ message: { conversation, id: cmd.trigger_id },
246
+ event: cmd,
247
+ content: formatSlashContent(cmd),
248
+ sender: { id: cmd.user_id, name: cmd.user_name },
249
+ endpointId: this.#options.config.id,
250
+ metadata: Object.freeze({
251
+ eventType: 'slash_command',
252
+ command: cmd.command,
253
+ }),
254
+ }).catch((err) => {
255
+ this.#logger.warn(formatCompact({
256
+ op: 'slack_gateway_receive_failed',
257
+ target: `${conversation.kind}:${conversation.id}`,
258
+ error: err instanceof Error ? err.message : String(err),
259
+ }));
260
+ });
261
+ }).catch(() => this.#logger.debug('Inbound callback did not complete'));
267
262
  }
268
263
  handleEnvelope(body) {
269
- const envelope = body;
270
- if (envelope?.type === 'event_callback' && envelope.event) {
271
- const event = envelope.event;
272
- if (event.type !== 'message' && event.type !== 'app_mention') {
273
- void this.#emitPlatformEvent(event.type || 'event', event);
274
- receiveSlackSideEvent((name, payload) => this.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
275
- return;
264
+ void this[endpointSpec].events.run(() => {
265
+ const envelope = body;
266
+ if (envelope?.type === 'event_callback' && envelope.event) {
267
+ const event = envelope.event;
268
+ if (event.type !== 'message' && event.type !== 'app_mention') {
269
+ void this.#emitPlatformEvent(event.type || 'event', event);
270
+ receiveSlackSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
271
+ return;
272
+ }
273
+ this.admit(event, body);
276
274
  }
277
- this.admit(event);
278
- }
275
+ }).catch(() => this.#logger.debug('Inbound callback did not complete'));
279
276
  }
280
277
  trackMessageChannel(ts, channel) {
281
278
  if (!ts || !channel)
@@ -302,22 +299,22 @@ export class SlackEndpoint extends Endpoint {
302
299
  return channel ? { channel, ts: messageId } : null;
303
300
  }
304
301
  async recallMessage(messageId) {
305
- if (!this.#client)
302
+ if (!this.#started)
306
303
  return;
307
304
  const ref = this.resolveMessageRef(messageId);
308
305
  if (!ref)
309
306
  return;
310
- await this.#client.chat.delete({ channel: ref.channel, ts: ref.ts });
307
+ await this.chat.delete({ channel: ref.channel, ts: ref.ts });
311
308
  }
312
- async #editMessage(channel, messageTs, content) {
313
- if (!this.#client)
309
+ async #editMessage(request) {
310
+ if (!this.#started)
314
311
  throw new Error('Slack client not connected');
315
- await editSlackContent(this.#client, channel, messageTs, content);
312
+ await editSlackContent(this, request);
316
313
  }
317
- async #addReaction(channel, timestamp, name) {
314
+ async #addReaction({ channel, timestamp, name, signal }) {
318
315
  const reaction = normalizeSlackReactionName(name);
319
316
  try {
320
- await this.#client.reactions.add({ channel, timestamp, name: reaction });
317
+ await createEndpointOperation({ signal }).run(() => this.reactions.add({ channel, timestamp, name: reaction }));
321
318
  }
322
319
  catch (error) {
323
320
  const code = error?.data?.error;
@@ -326,10 +323,10 @@ export class SlackEndpoint extends Endpoint {
326
323
  throw error;
327
324
  }
328
325
  }
329
- async #removeReaction(channel, timestamp, name) {
326
+ async #removeReaction({ channel, timestamp, name, signal }) {
330
327
  const reaction = normalizeSlackReactionName(name);
331
328
  try {
332
- await this.#client.reactions.remove({ channel, timestamp, name: reaction });
329
+ await createEndpointOperation({ signal }).run(() => this.reactions.remove({ channel, timestamp, name: reaction }));
333
330
  }
334
331
  catch (error) {
335
332
  const code = error?.data?.error;
@@ -339,7 +336,7 @@ export class SlackEndpoint extends Endpoint {
339
336
  }
340
337
  }
341
338
  async #emitPlatformEvent(name, event) {
342
- await this.emitPlatform(name, event).catch((error) => {
339
+ await this[endpointSpec].events.platform(name, event).catch((error) => {
343
340
  this.#logger.warn(formatCompact({
344
341
  op: 'slack_platform_event_failed',
345
342
  event: name,
@@ -347,6 +344,18 @@ export class SlackEndpoint extends Endpoint {
347
344
  }));
348
345
  });
349
346
  }
347
+ #awaitSocketAck(ack) {
348
+ const signal = this.#executionSignal;
349
+ if (!signal)
350
+ return ack;
351
+ return new Promise((resolve, reject) => {
352
+ const abort = () => reject(signal.reason);
353
+ signal.addEventListener('abort', abort, { once: true });
354
+ void ack.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
355
+ if (signal.aborted)
356
+ abort();
357
+ });
358
+ }
350
359
  async #startSocket() {
351
360
  const { config } = this.#options;
352
361
  if (!config.appToken) {
@@ -371,15 +380,57 @@ export class SlackEndpoint extends Endpoint {
371
380
  return; release(); handle.notifyClosed('Slack socket disconnected'); });
372
381
  socket.on('error', error => { connectionError = slackSocketConnectionError(error); if (!active)
373
382
  return; release(); handle.notifyClosed('Slack socket error'); });
374
- socket.on('slack_event', async ({ ack, body }) => { if (!active || this.#socket !== socket)
375
- return; await ack(); if (active)
376
- this.handleEnvelope(body); });
377
- socket.on('interactive', async ({ ack, body }) => { if (!active || this.#socket !== socket)
378
- return; await ack(); if (active)
379
- this.admitInteraction(body); });
380
- socket.on('slash_commands', async ({ ack, body }) => { if (!active || this.#socket !== socket)
381
- return; await ack(); if (active)
382
- this.admitSlashCommand(body); });
383
+ socket.on('slack_event', ({ ack, body }) => {
384
+ if (!active || this.#socket !== socket)
385
+ return;
386
+ void this[endpointSpec].events.run(async () => {
387
+ try {
388
+ await this.#awaitSocketAck(ack());
389
+ }
390
+ catch (error) {
391
+ if (!this.#executionSignal?.aborted)
392
+ this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
393
+ throw error;
394
+ }
395
+ this.#executionSignal?.throwIfAborted();
396
+ if (active)
397
+ this.handleEnvelope(body);
398
+ }).catch(() => this.#logger.debug('Socket callback did not complete'));
399
+ });
400
+ socket.on('interactive', ({ ack, body }) => {
401
+ if (!active || this.#socket !== socket)
402
+ return;
403
+ void this[endpointSpec].events.run(async () => {
404
+ try {
405
+ await this.#awaitSocketAck(ack());
406
+ }
407
+ catch (error) {
408
+ if (!this.#executionSignal?.aborted)
409
+ this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
410
+ throw error;
411
+ }
412
+ this.#executionSignal?.throwIfAborted();
413
+ if (active)
414
+ this.admitInteraction(body);
415
+ }).catch(() => this.#logger.debug('Socket callback did not complete'));
416
+ });
417
+ socket.on('slash_commands', ({ ack, body }) => {
418
+ if (!active || this.#socket !== socket)
419
+ return;
420
+ void this[endpointSpec].events.run(async () => {
421
+ try {
422
+ await this.#awaitSocketAck(ack());
423
+ }
424
+ catch (error) {
425
+ if (!this.#executionSignal?.aborted)
426
+ this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
427
+ throw error;
428
+ }
429
+ this.#executionSignal?.throwIfAborted();
430
+ if (active)
431
+ this.admitSlashCommand(body);
432
+ }).catch(() => this.#logger.debug('Socket callback did not complete'));
433
+ });
383
434
  try {
384
435
  await socket.start();
385
436
  }
@@ -396,17 +447,18 @@ function createSlackEndpointManagement(requireClient) {
396
447
  return Object.freeze({
397
448
  // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
398
449
  // Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
399
- async listGroups() {
450
+ async listGroups(request = {}) {
451
+ const operation = createEndpointOperation(request);
400
452
  const client = requireClient();
401
453
  const groups = [];
402
454
  let cursor;
403
455
  do {
404
- const page = await client.conversations.list({
456
+ const page = await operation.run(() => client.conversations.list({
405
457
  types: 'public_channel',
406
458
  exclude_archived: true,
407
459
  limit: SLACK_LIST_PAGE_SIZE,
408
460
  ...(cursor ? { cursor } : {}),
409
- });
461
+ }));
410
462
  for (const value of page.channels ?? []) {
411
463
  const channel = asRecord(value);
412
464
  const id = typeof channel.id === 'string' ? channel.id : '';
@@ -419,15 +471,16 @@ function createSlackEndpointManagement(requireClient) {
419
471
  return groups;
420
472
  },
421
473
  // Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
422
- async listFriends() {
474
+ async listFriends(request = {}) {
475
+ const operation = createEndpointOperation(request);
423
476
  const client = requireClient();
424
477
  const friends = [];
425
478
  let cursor;
426
479
  do {
427
- const page = await client.users.list({
480
+ const page = await operation.run(() => client.users.list({
428
481
  limit: SLACK_LIST_PAGE_SIZE,
429
482
  ...(cursor ? { cursor } : {}),
430
- });
483
+ }));
431
484
  for (const value of page.members ?? []) {
432
485
  const user = asRecord(value);
433
486
  const id = typeof user.id === 'string' ? user.id : '';
@@ -445,9 +498,10 @@ function createSlackEndpointManagement(requireClient) {
445
498
  return friends;
446
499
  },
447
500
  // conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
448
- async listGroupMembers(groupId) {
501
+ async listGroupMembers({ groupId, signal }) {
502
+ const operation = createEndpointOperation({ signal });
449
503
  const client = requireClient();
450
- const result = await client.conversations.members({ channel: groupId });
504
+ const result = await operation.run(() => client.conversations.members({ channel: groupId }));
451
505
  return result.members ?? [];
452
506
  },
453
507
  });
package/lib/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, slackInboundConversation, verifySlackSignature, type ResolvedSlackConfig, type SlackEndpointConfig, 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
- export { slackClient, type SlackClientEventMap, type SlackUserInfo, } from './client.js';
4
+ export { type SlackClientEventMap, type SlackUserInfo, } from './client.js';
5
5
  export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
6
6
  export { normalizeSlackReactionName } from './slack-reaction.js';
7
7
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
8
8
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
9
9
  export { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
10
10
  export { editSlackContent, sendSlackContent } from './slack-outbound.js';
11
+ export { SlackAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
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
- export { slackClient, } from './client.js';
5
4
  export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
6
5
  export { normalizeSlackReactionName } from './slack-reaction.js';
7
6
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
8
7
  export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
9
8
  export { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
10
9
  export { editSlackContent, sendSlackContent } from './slack-outbound.js';
10
+ export { SlackAdapter } from './adapter.js';