@zhin.js/adapter-slack 7.0.0 → 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
  */
@@ -7,11 +8,9 @@ import type {
7
8
  EndpointFriend,
8
9
  EndpointGroup,
9
10
  EndpointControl,
10
- EndpointInstance,
11
11
  EndpointManagement,
12
12
  EndpointSendRequest,
13
13
  } from 'zhin.js/adapter';
14
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
15
14
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
16
15
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
17
16
  import type { CapabilityId } from 'zhin.js';
@@ -28,7 +27,6 @@ import {
28
27
  type SlackMessageEvent,
29
28
  type SlackSlashCommand,
30
29
  } from './protocol.js';
31
- import { registerSlackAgentEndpoint, type SlackUserInfo } from './slack-agent-deps.js';
32
30
  import {
33
31
  createSlackInboundFilterState,
34
32
  shouldDropSlackInboundMessage,
@@ -92,8 +90,6 @@ export interface SlackWebClientLike extends SlackChatClient {
92
90
 
93
91
  export interface SlackEndpointOptions {
94
92
  readonly id: CapabilityId;
95
- readonly gateway: MessageGateway;
96
- readonly sideEvents?: SideEventGateway;
97
93
  readonly config: ResolvedSlackConfig;
98
94
  readonly http?: HttpHost;
99
95
  readonly createClient?: (token: string) => SlackWebClientLike;
@@ -103,7 +99,7 @@ export interface SlackEndpointOptions {
103
99
  }) => SlackSocketLike;
104
100
  }
105
101
 
106
- export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
102
+ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements SlackWebhookHandler {
107
103
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
108
104
 
109
105
  readonly #options: SlackEndpointOptions;
@@ -115,8 +111,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
115
111
  #botUserId?: string;
116
112
  #open = false;
117
113
  #started = false;
118
- #unregisterAgent?: () => void;
119
- readonly management: EndpointManagement = createSlackEndpointManagement(this);
114
+ readonly management: EndpointManagement = createSlackEndpointManagement(() => this.client);
120
115
  readonly control: EndpointControl = Object.freeze<EndpointControl>({
121
116
  recall: async (message) => {
122
117
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
@@ -126,24 +121,25 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
126
121
  edit: async (message, content) => {
127
122
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
128
123
  if (!ref) return null;
129
- await this.editMessage(ref.channel, ref.ts, content);
124
+ await this.#editMessage(ref.channel, ref.ts, content);
130
125
  return message.id;
131
126
  },
132
127
  addReaction: async (message, emoji) => {
133
128
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
134
129
  if (!ref) return null;
135
130
  const reaction = normalizeSlackReactionName(emoji);
136
- await this.addReaction(ref.channel, ref.ts, reaction);
131
+ await this.#addReaction(ref.channel, ref.ts, reaction);
137
132
  return reaction;
138
133
  },
139
134
  removeReaction: async (message, reactionId) => {
140
135
  const ref = this.resolveMessageRef(message.id, message.conversation.id);
141
136
  if (!ref) return;
142
- await this.removeReaction(ref.channel, ref.ts, reactionId);
137
+ await this.#removeReaction(ref.channel, ref.ts, reactionId);
143
138
  },
144
139
  });
145
140
 
146
141
  constructor(options: SlackEndpointOptions) {
142
+ super();
147
143
  this.#logger = getAdapterLogger('slack', options.config.id);
148
144
  this.#options = options;
149
145
  }
@@ -153,7 +149,8 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
153
149
  return this.#options.config.id;
154
150
  }
155
151
 
156
- get client(): SlackWebClientLike | undefined {
152
+ get client(): SlackWebClientLike {
153
+ if (!this.#client) throw new Error('Slack client not connected');
157
154
  return this.#client;
158
155
  }
159
156
 
@@ -173,8 +170,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
173
170
  this.#client = this.#options.createClient?.(config.token)
174
171
  ?? (new WebClient(config.token) as unknown as SlackWebClientLike);
175
172
 
176
- this.#unregisterAgent = registerSlackAgentEndpoint(config.id, this);
177
-
178
173
  if (config.mode === 'socket') {
179
174
  await this.#startSocket();
180
175
  } else {
@@ -223,8 +218,6 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
223
218
  this.#socket = undefined;
224
219
  }
225
220
  for (const release of this.#routeReleases.splice(0)) release();
226
- this.#unregisterAgent?.();
227
- this.#unregisterAgent = undefined;
228
221
  this.#client = undefined;
229
222
  this.#started = false;
230
223
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
@@ -247,6 +240,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
247
240
  /** Test / internal: admit a message event when open. */
248
241
  admit(event: SlackMessageEvent | SlackEvent): void {
249
242
  if (!this.#open) return;
243
+ void this.#emitPlatformEvent(event.type || 'event', event);
250
244
  if (event.type !== 'message' && event.type !== 'app_mention') return;
251
245
  const msg = event as SlackMessageEvent;
252
246
  if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
@@ -259,7 +253,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
259
253
  channelType: msg.channel_type,
260
254
  threadId: threadTs,
261
255
  });
262
- void this.#options.gateway.receive({
256
+ void this.emit('message.receive', {
263
257
  conversation,
264
258
  message: { conversation, id: msg.ts },
265
259
  content: formatInboundContent(msg),
@@ -283,6 +277,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
283
277
 
284
278
  admitInteraction(payload: SlackInteractionPayload): void {
285
279
  if (!this.#open) return;
280
+ void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
286
281
  if (payload.type !== 'block_actions' || !payload.actions?.length) return;
287
282
  const channelId = payload.channel?.id ?? '';
288
283
  const userId = payload.user.id;
@@ -296,7 +291,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
296
291
  // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
297
292
  channelType: channelId ? undefined : 'im',
298
293
  });
299
- void this.#options.gateway.receive({
294
+ void this.emit('message.receive', {
300
295
  conversation,
301
296
  message: { conversation, id: actionTs },
302
297
  content: formatInteractionContent(payload),
@@ -318,11 +313,12 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
318
313
 
319
314
  admitSlashCommand(cmd: SlackSlashCommand): void {
320
315
  if (!this.#open) return;
316
+ void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
321
317
  postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
322
318
  const conversation = slackInboundConversation(String(this.#options.id), {
323
319
  channelId: cmd.channel_id,
324
320
  });
325
- void this.#options.gateway.receive({
321
+ void this.emit('message.receive', {
326
322
  conversation,
327
323
  message: { conversation, id: cmd.trigger_id },
328
324
  content: formatSlashContent(cmd),
@@ -346,8 +342,9 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
346
342
  if (envelope?.type === 'event_callback' && envelope.event) {
347
343
  const event = envelope.event;
348
344
  if (event.type !== 'message' && event.type !== 'app_mention') {
345
+ void this.#emitPlatformEvent(event.type || 'event', event);
349
346
  receiveSlackSideEvent(
350
- this.#options.sideEvents,
347
+ (name, payload) => this.emit(name, payload),
351
348
  String(this.#options.id),
352
349
  this.#options.config.id,
353
350
  event,
@@ -388,95 +385,41 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
388
385
  await this.#client.chat.delete({ channel: ref.channel, ts: ref.ts });
389
386
  }
390
387
 
391
- async editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
388
+ async #editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
392
389
  if (!this.#client) throw new Error('Slack client not connected');
393
390
  await editSlackContent(this.#client, channel, messageTs, content);
394
391
  }
395
392
 
396
- // ── Agent tool surface ──────────────────────────────────────────────
397
-
398
- async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
399
- await this.#client!.conversations.invite({ channel, users: users.join(',') });
400
- return true;
401
- }
402
-
403
- async kickFromChannel(channel: string, user: string): Promise<boolean> {
404
- await this.#client!.conversations.kick({ channel, user });
405
- return true;
406
- }
407
-
408
- async setChannelTopic(channel: string, topic: string): Promise<boolean> {
409
- await this.#client!.conversations.setTopic({ channel, topic });
410
- return true;
411
- }
412
-
413
- async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
414
- await this.#client!.conversations.setPurpose({ channel, purpose });
415
- return true;
416
- }
417
-
418
- async archiveChannel(channel: string): Promise<boolean> {
419
- await this.#client!.conversations.archive({ channel });
420
- return true;
421
- }
422
-
423
- async unarchiveChannel(channel: string): Promise<boolean> {
424
- await this.#client!.conversations.unarchive({ channel });
425
- return true;
426
- }
427
-
428
- async renameChannel(channel: string, name: string): Promise<boolean> {
429
- await this.#client!.conversations.rename({ channel, name });
430
- return true;
431
- }
432
-
433
- async getChannelMembers(channel: string): Promise<string[]> {
434
- const result = await this.#client!.conversations.members({ channel });
435
- return result.members || [];
436
- }
437
-
438
- async getChannelInfo(channel: string): Promise<unknown> {
439
- const result = await this.#client!.conversations.info({ channel });
440
- return result.channel;
441
- }
442
-
443
- async getUserInfo(user: string): Promise<SlackUserInfo | undefined> {
444
- const result = await this.#client!.users.info({ user });
445
- return result.user as SlackUserInfo | undefined;
446
- }
447
-
448
- async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
393
+ async #addReaction(channel: string, timestamp: string, name: string): Promise<void> {
449
394
  const reaction = normalizeSlackReactionName(name);
450
395
  try {
451
396
  await this.#client!.reactions.add({ channel, timestamp, name: reaction });
452
- return true;
453
397
  } catch (error) {
454
398
  const code = (error as { data?: { error?: string } })?.data?.error;
455
- if (code === 'already_reacted') return true;
399
+ if (code === 'already_reacted') return;
456
400
  throw error;
457
401
  }
458
402
  }
459
403
 
460
- async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
404
+ async #removeReaction(channel: string, timestamp: string, name: string): Promise<void> {
461
405
  const reaction = normalizeSlackReactionName(name);
462
406
  try {
463
407
  await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
464
- return true;
465
408
  } catch (error) {
466
409
  const code = (error as { data?: { error?: string } })?.data?.error;
467
- if (code === 'no_reaction') return true;
410
+ if (code === 'no_reaction') return;
468
411
  throw error;
469
412
  }
470
413
  }
471
414
 
472
- async pinMessage(channel: string, timestamp: string): Promise<boolean> {
473
- await this.#client!.pins.add({ channel, timestamp });
474
- return true;
475
- }
476
-
477
- async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
478
- await this.#client!.pins.remove({ channel, timestamp });
479
- 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
+ });
480
423
  }
481
424
 
482
425
  async #startSocket(): Promise<void> {
@@ -515,12 +458,9 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
515
458
  /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
516
459
  const SLACK_LIST_PAGE_SIZE = 1000;
517
460
 
518
- function createSlackEndpointManagement(endpoint: SlackEndpoint): EndpointManagement {
519
- const requireClient = (): SlackWebClientLike => {
520
- const client = endpoint.client;
521
- if (!client) throw new Error('Slack client not connected');
522
- return client;
523
- };
461
+ function createSlackEndpointManagement(
462
+ requireClient: () => SlackWebClientLike,
463
+ ): EndpointManagement {
524
464
  return Object.freeze<EndpointManagement>({
525
465
  // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
526
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
- }