@zhin.js/adapter-slack 6.0.14 → 7.0.1

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/src/endpoint.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
3
4
  */
@@ -6,11 +7,10 @@ import { WebClient } from '@slack/web-api';
6
7
  import type {
7
8
  EndpointFriend,
8
9
  EndpointGroup,
9
- EndpointInstance,
10
+ EndpointControl,
10
11
  EndpointManagement,
11
12
  EndpointSendRequest,
12
13
  } from 'zhin.js/adapter';
13
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
14
14
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
15
15
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
16
  import type { CapabilityId } from 'zhin.js';
@@ -27,7 +27,6 @@ import {
27
27
  type SlackMessageEvent,
28
28
  type SlackSlashCommand,
29
29
  } from './protocol.js';
30
- import { registerSlackAgentEndpoint, type SlackUserInfo } from './slack-agent-deps.js';
31
30
  import {
32
31
  createSlackInboundFilterState,
33
32
  shouldDropSlackInboundMessage,
@@ -91,8 +90,6 @@ export interface SlackWebClientLike extends SlackChatClient {
91
90
 
92
91
  export interface SlackEndpointOptions {
93
92
  readonly id: CapabilityId;
94
- readonly gateway: MessageGateway;
95
- readonly sideEvents?: SideEventGateway;
96
93
  readonly config: ResolvedSlackConfig;
97
94
  readonly http?: HttpHost;
98
95
  readonly createClient?: (token: string) => SlackWebClientLike;
@@ -102,7 +99,7 @@ export interface SlackEndpointOptions {
102
99
  }) => SlackSocketLike;
103
100
  }
104
101
 
105
- export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
102
+ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements SlackWebhookHandler {
106
103
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
107
104
 
108
105
  readonly #options: SlackEndpointOptions;
@@ -114,10 +111,35 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
114
111
  #botUserId?: string;
115
112
  #open = false;
116
113
  #started = false;
117
- #unregisterAgent?: () => void;
118
- readonly management: EndpointManagement = createSlackEndpointManagement(this);
114
+ readonly management: EndpointManagement = createSlackEndpointManagement(() => this.client);
115
+ readonly control: EndpointControl = Object.freeze<EndpointControl>({
116
+ recall: async (message) => {
117
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
118
+ if (!ref || !this.#client) return;
119
+ await this.#client.chat.delete(ref);
120
+ },
121
+ edit: async (message, content) => {
122
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
123
+ if (!ref) return null;
124
+ await this.#editMessage(ref.channel, ref.ts, content);
125
+ return message.id;
126
+ },
127
+ addReaction: async (message, emoji) => {
128
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
129
+ if (!ref) return null;
130
+ const reaction = normalizeSlackReactionName(emoji);
131
+ await this.#addReaction(ref.channel, ref.ts, reaction);
132
+ return reaction;
133
+ },
134
+ removeReaction: async (message, reactionId) => {
135
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
136
+ if (!ref) return;
137
+ await this.#removeReaction(ref.channel, ref.ts, reactionId);
138
+ },
139
+ });
119
140
 
120
141
  constructor(options: SlackEndpointOptions) {
142
+ super();
121
143
  this.#logger = getAdapterLogger('slack', options.config.id);
122
144
  this.#options = options;
123
145
  }
@@ -127,7 +149,8 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
127
149
  return this.#options.config.id;
128
150
  }
129
151
 
130
- get client(): SlackWebClientLike | undefined {
152
+ get client(): SlackWebClientLike {
153
+ if (!this.#client) throw new Error('Slack client not connected');
131
154
  return this.#client;
132
155
  }
133
156
 
@@ -147,8 +170,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
147
170
  this.#client = this.#options.createClient?.(config.token)
148
171
  ?? (new WebClient(config.token) as unknown as SlackWebClientLike);
149
172
 
150
- this.#unregisterAgent = registerSlackAgentEndpoint(config.id, this);
151
-
152
173
  if (config.mode === 'socket') {
153
174
  await this.#startSocket();
154
175
  } else {
@@ -197,8 +218,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
197
218
  this.#socket = undefined;
198
219
  }
199
220
  for (const release of this.#routeReleases.splice(0)) release();
200
- this.#unregisterAgent?.();
201
- this.#unregisterAgent = undefined;
202
221
  this.#client = undefined;
203
222
  this.#started = false;
204
223
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
@@ -221,6 +240,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
221
240
  /** Test / internal: admit a message event when open. */
222
241
  admit(event: SlackMessageEvent | SlackEvent): void {
223
242
  if (!this.#open) return;
243
+ void this.#emitPlatformEvent(event.type || 'event', event);
224
244
  if (event.type !== 'message' && event.type !== 'app_mention') return;
225
245
  const msg = event as SlackMessageEvent;
226
246
  if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
@@ -233,7 +253,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
233
253
  channelType: msg.channel_type,
234
254
  threadId: threadTs,
235
255
  });
236
- void this.#options.gateway.receive({
256
+ void this.emit('message.receive', {
237
257
  conversation,
238
258
  message: { conversation, id: msg.ts },
239
259
  content: formatInboundContent(msg),
@@ -257,6 +277,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
257
277
 
258
278
  admitInteraction(payload: SlackInteractionPayload): void {
259
279
  if (!this.#open) return;
280
+ void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
260
281
  if (payload.type !== 'block_actions' || !payload.actions?.length) return;
261
282
  const channelId = payload.channel?.id ?? '';
262
283
  const userId = payload.user.id;
@@ -270,7 +291,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
270
291
  // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
271
292
  channelType: channelId ? undefined : 'im',
272
293
  });
273
- void this.#options.gateway.receive({
294
+ void this.emit('message.receive', {
274
295
  conversation,
275
296
  message: { conversation, id: actionTs },
276
297
  content: formatInteractionContent(payload),
@@ -292,11 +313,12 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
292
313
 
293
314
  admitSlashCommand(cmd: SlackSlashCommand): void {
294
315
  if (!this.#open) return;
316
+ void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
295
317
  postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
296
318
  const conversation = slackInboundConversation(String(this.#options.id), {
297
319
  channelId: cmd.channel_id,
298
320
  });
299
- void this.#options.gateway.receive({
321
+ void this.emit('message.receive', {
300
322
  conversation,
301
323
  message: { conversation, id: cmd.trigger_id },
302
324
  content: formatSlashContent(cmd),
@@ -320,8 +342,9 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
320
342
  if (envelope?.type === 'event_callback' && envelope.event) {
321
343
  const event = envelope.event;
322
344
  if (event.type !== 'message' && event.type !== 'app_mention') {
345
+ void this.#emitPlatformEvent(event.type || 'event', event);
323
346
  receiveSlackSideEvent(
324
- this.#options.sideEvents,
347
+ (name, payload) => this.emit(name, payload),
325
348
  String(this.#options.id),
326
349
  this.#options.config.id,
327
350
  event,
@@ -362,95 +385,41 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
362
385
  await this.#client.chat.delete({ channel: ref.channel, ts: ref.ts });
363
386
  }
364
387
 
365
- async editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
388
+ async #editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
366
389
  if (!this.#client) throw new Error('Slack client not connected');
367
390
  await editSlackContent(this.#client, channel, messageTs, content);
368
391
  }
369
392
 
370
- // ── Agent tool surface ──────────────────────────────────────────────
371
-
372
- async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
373
- await this.#client!.conversations.invite({ channel, users: users.join(',') });
374
- return true;
375
- }
376
-
377
- async kickFromChannel(channel: string, user: string): Promise<boolean> {
378
- await this.#client!.conversations.kick({ channel, user });
379
- return true;
380
- }
381
-
382
- async setChannelTopic(channel: string, topic: string): Promise<boolean> {
383
- await this.#client!.conversations.setTopic({ channel, topic });
384
- return true;
385
- }
386
-
387
- async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
388
- await this.#client!.conversations.setPurpose({ channel, purpose });
389
- return true;
390
- }
391
-
392
- async archiveChannel(channel: string): Promise<boolean> {
393
- await this.#client!.conversations.archive({ channel });
394
- return true;
395
- }
396
-
397
- async unarchiveChannel(channel: string): Promise<boolean> {
398
- await this.#client!.conversations.unarchive({ channel });
399
- return true;
400
- }
401
-
402
- async renameChannel(channel: string, name: string): Promise<boolean> {
403
- await this.#client!.conversations.rename({ channel, name });
404
- return true;
405
- }
406
-
407
- async getChannelMembers(channel: string): Promise<string[]> {
408
- const result = await this.#client!.conversations.members({ channel });
409
- return result.members || [];
410
- }
411
-
412
- async getChannelInfo(channel: string): Promise<unknown> {
413
- const result = await this.#client!.conversations.info({ channel });
414
- return result.channel;
415
- }
416
-
417
- async getUserInfo(user: string): Promise<SlackUserInfo | undefined> {
418
- const result = await this.#client!.users.info({ user });
419
- return result.user as SlackUserInfo | undefined;
420
- }
421
-
422
- async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
393
+ async #addReaction(channel: string, timestamp: string, name: string): Promise<void> {
423
394
  const reaction = normalizeSlackReactionName(name);
424
395
  try {
425
396
  await this.#client!.reactions.add({ channel, timestamp, name: reaction });
426
- return true;
427
397
  } catch (error) {
428
398
  const code = (error as { data?: { error?: string } })?.data?.error;
429
- if (code === 'already_reacted') return true;
399
+ if (code === 'already_reacted') return;
430
400
  throw error;
431
401
  }
432
402
  }
433
403
 
434
- async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
404
+ async #removeReaction(channel: string, timestamp: string, name: string): Promise<void> {
435
405
  const reaction = normalizeSlackReactionName(name);
436
406
  try {
437
407
  await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
438
- return true;
439
408
  } catch (error) {
440
409
  const code = (error as { data?: { error?: string } })?.data?.error;
441
- if (code === 'no_reaction') return true;
410
+ if (code === 'no_reaction') return;
442
411
  throw error;
443
412
  }
444
413
  }
445
414
 
446
- async pinMessage(channel: string, timestamp: string): Promise<boolean> {
447
- await this.#client!.pins.add({ channel, timestamp });
448
- return true;
449
- }
450
-
451
- async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
452
- await this.#client!.pins.remove({ channel, timestamp });
453
- return true;
415
+ async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
416
+ await this.emitPlatform(name, event).catch((error) => {
417
+ this.#logger.warn(formatCompact({
418
+ op: 'slack_platform_event_failed',
419
+ event: name,
420
+ error: error instanceof Error ? error.message : String(error),
421
+ }));
422
+ });
454
423
  }
455
424
 
456
425
  async #startSocket(): Promise<void> {
@@ -489,12 +458,9 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
489
458
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
490
459
  const SLACK_LIST_PAGE_SIZE = 1000;
491
460
 
492
- function createSlackEndpointManagement(endpoint: SlackEndpoint): EndpointManagement {
493
- const requireClient = (): SlackWebClientLike => {
494
- const client = endpoint.client;
495
- if (!client) throw new Error('Slack client not connected');
496
- return client;
497
- };
461
+ function createSlackEndpointManagement(
462
+ requireClient: () => SlackWebClientLike,
463
+ ): EndpointManagement {
498
464
  return Object.freeze<EndpointManagement>({
499
465
  // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
500
466
  // Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
package/src/index.ts CHANGED
@@ -37,12 +37,10 @@ export {
37
37
  } from './webhook.js';
38
38
 
39
39
  export {
40
- getSlackAgentDeps,
41
- registerSlackAgentEndpoint,
42
- setSlackAgentDeps,
43
- type SlackAgentDeps,
44
- type SlackAgentEndpoint,
45
- } from './slack-agent-deps.js';
40
+ slackClient,
41
+ type SlackClientEventMap,
42
+ type SlackUserInfo,
43
+ } from './client.js';
46
44
 
47
45
  export {
48
46
  checkSlackPlatformPermit,
package/src/protocol.ts CHANGED
@@ -217,7 +217,7 @@ export function slackInboundConversation(
217
217
  };
218
218
  }
219
219
 
220
- /** Build inbound text for MessageGateway.receive. */
220
+ /** Build inbound text for OutboundMessageService.receive. */
221
221
  export function formatInboundContent(event: SlackMessageEvent | SlackEvent): string {
222
222
  const text = typeof event.text === 'string' ? event.text : '';
223
223
  if (text) return mrkdwnToMarkdown(text);
@@ -1,16 +1,16 @@
1
1
  import { buildNotice, mapNoticeParts, senderFromId, SLACK_NOTICE_PARTS_MAP } from '@zhin.js/core';
2
- import type { SideEventGateway } from '@zhin.js/core/runtime';
2
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
3
3
  import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
4
4
  import type { SlackEvent } from './protocol.js';
5
5
 
6
6
  export function receiveSlackSideEvent(
7
- sideEvents: SideEventGateway | undefined,
7
+ emit: EndpointEventEmitter,
8
8
  endpointKey: string,
9
9
  configId: string,
10
10
  event: SlackEvent,
11
11
  logger: ReturnType<typeof getAdapterLogger>,
12
12
  ): void {
13
- if (!sideEvents) return;
13
+ if (!emit) return;
14
14
  const eventType = String(event.type ?? '');
15
15
  if (!Object.prototype.hasOwnProperty.call(SLACK_NOTICE_PARTS_MAP, eventType)) return;
16
16
  const record = event as Record<string, unknown>;
@@ -24,7 +24,7 @@ export function receiveSlackSideEvent(
24
24
  user ?? '',
25
25
  String(record.ts ?? record.event_ts ?? ''),
26
26
  ].join(':');
27
- void sideEvents.receiveNotice(buildNotice(record, {
27
+ void emit('notice.receive', buildNotice(record, {
28
28
  $id: `slack:${dedupeKey}`,
29
29
  $adapter: 'slack' as never,
30
30
  $endpoint: configId,
@@ -1,41 +0,0 @@
1
- /**
2
- * Agent tool deps for slack.
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- export interface SlackAgentEndpoint {
6
- inviteToChannel(channel: string, users: string[]): Promise<boolean>;
7
- kickFromChannel(channel: string, user: string): Promise<boolean>;
8
- setChannelTopic(channel: string, topic: string): Promise<boolean>;
9
- setChannelPurpose(channel: string, purpose: string): Promise<boolean>;
10
- archiveChannel(channel: string): Promise<boolean>;
11
- unarchiveChannel(channel: string): Promise<boolean>;
12
- renameChannel(channel: string, name: string): Promise<boolean>;
13
- getChannelMembers(channel: string): Promise<string[]>;
14
- getChannelInfo(channel: string): Promise<unknown>;
15
- getUserInfo(user: string): Promise<SlackUserInfo | undefined>;
16
- addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
17
- removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
18
- pinMessage(channel: string, timestamp: string): Promise<boolean>;
19
- unpinMessage(channel: string, timestamp: string): Promise<boolean>;
20
- editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
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
- }
35
- export interface SlackAgentDeps {
36
- getEndpoint: (endpointKey: string) => SlackAgentEndpoint;
37
- }
38
- export declare function registerSlackAgentEndpoint(endpointKey: string, endpoint: SlackAgentEndpoint): () => void;
39
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
40
- export declare function setSlackAgentDeps(deps: SlackAgentDeps | null): void;
41
- export declare function getSlackAgentDeps(): SlackAgentDeps;
@@ -1,30 +0,0 @@
1
- /**
2
- * Agent tool deps for slack.
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- const endpoints = new Map();
6
- let override = null;
7
- export function registerSlackAgentEndpoint(endpointKey, endpoint) {
8
- endpoints.set(endpointKey, endpoint);
9
- return () => {
10
- if (endpoints.get(endpointKey) === endpoint) {
11
- endpoints.delete(endpointKey);
12
- }
13
- };
14
- }
15
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
- export function setSlackAgentDeps(deps) {
17
- override = deps;
18
- }
19
- export function getSlackAgentDeps() {
20
- if (override)
21
- return override;
22
- return {
23
- getEndpoint(endpointKey) {
24
- const registered = endpoints.get(endpointKey);
25
- if (!registered)
26
- throw new Error(`Endpoint ${endpointKey} 不存在`);
27
- return registered;
28
- },
29
- };
30
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Agent tool deps for slack.
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
-
6
- export interface SlackAgentEndpoint {
7
- inviteToChannel(channel: string, users: string[]): Promise<boolean>;
8
- kickFromChannel(channel: string, user: string): Promise<boolean>;
9
- setChannelTopic(channel: string, topic: string): Promise<boolean>;
10
- setChannelPurpose(channel: string, purpose: string): Promise<boolean>;
11
- archiveChannel(channel: string): Promise<boolean>;
12
- unarchiveChannel(channel: string): Promise<boolean>;
13
- renameChannel(channel: string, name: string): Promise<boolean>;
14
- getChannelMembers(channel: string): Promise<string[]>;
15
- getChannelInfo(channel: string): Promise<unknown>;
16
- getUserInfo(user: string): Promise<SlackUserInfo | undefined>;
17
- addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
18
- removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
19
- pinMessage(channel: string, timestamp: string): Promise<boolean>;
20
- unpinMessage(channel: string, timestamp: string): Promise<boolean>;
21
- editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
22
- }
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
-
38
- export interface SlackAgentDeps {
39
- getEndpoint: (endpointKey: string) => SlackAgentEndpoint;
40
- }
41
-
42
- const endpoints = new Map<string, SlackAgentEndpoint>();
43
- let override: SlackAgentDeps | null = null;
44
-
45
- export function registerSlackAgentEndpoint(
46
- endpointKey: string,
47
- endpoint: SlackAgentEndpoint,
48
- ): () => void {
49
- endpoints.set(endpointKey, endpoint);
50
- return () => {
51
- if (endpoints.get(endpointKey) === endpoint) {
52
- endpoints.delete(endpointKey);
53
- }
54
- };
55
- }
56
-
57
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
58
- export function setSlackAgentDeps(deps: SlackAgentDeps | null): void {
59
- override = deps;
60
- }
61
-
62
- export function getSlackAgentDeps(): SlackAgentDeps {
63
- if (override) return override;
64
- return {
65
- getEndpoint(endpointKey: string): SlackAgentEndpoint {
66
- const registered = endpoints.get(endpointKey);
67
- if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
68
- return registered;
69
- },
70
- };
71
- }