@zhin.js/adapter-slack 4.1.0 → 5.0.0

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 (118) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +76 -252
  3. package/adapters/slack.ts +34 -0
  4. package/agent/tools/add_reaction.ts +2 -2
  5. package/agent/tools/archive_channel.ts +2 -2
  6. package/agent/tools/edit_message.ts +5 -4
  7. package/agent/tools/invite_to_channel.ts +2 -2
  8. package/agent/tools/pin_message.ts +2 -2
  9. package/agent/tools/remove_reaction.ts +2 -2
  10. package/agent/tools/set_purpose.ts +2 -2
  11. package/agent/tools/set_topic.ts +2 -2
  12. package/agent/tools/unarchive.ts +2 -2
  13. package/agent/tools/unpin_message.ts +2 -2
  14. package/agent/tools/user_info.ts +2 -2
  15. package/lib/endpoint.d.ts +149 -0
  16. package/lib/endpoint.js +339 -0
  17. package/lib/index.d.ts +11 -0
  18. package/lib/index.js +11 -0
  19. package/lib/markdown-to-mrkdwn.d.ts +9 -0
  20. package/lib/markdown-to-mrkdwn.js +121 -0
  21. package/lib/mrkdwn-to-markdown.d.ts +4 -0
  22. package/lib/{src/mrkdwn-to-markdown.js → mrkdwn-to-markdown.js} +0 -1
  23. package/lib/platform-permit.d.ts +16 -0
  24. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  25. package/lib/protocol.d.ts +143 -0
  26. package/lib/protocol.js +225 -0
  27. package/lib/slack-agent-deps.d.ts +28 -0
  28. package/lib/slack-agent-deps.js +30 -0
  29. package/lib/slack-inbound-filter.d.ts +12 -0
  30. package/lib/{src/slack-inbound-filter.js → slack-inbound-filter.js} +0 -1
  31. package/lib/slack-message-ref.d.ts +7 -0
  32. package/lib/{src/slack-message-ref.js → slack-message-ref.js} +0 -1
  33. package/lib/slack-outbound.d.ts +24 -0
  34. package/lib/slack-outbound.js +109 -0
  35. package/lib/slack-reaction.d.ts +1 -0
  36. package/lib/{src/slack-reaction.js → slack-reaction.js} +0 -1
  37. package/lib/slack-response-url.d.ts +5 -0
  38. package/lib/{src/slack-response-url.js → slack-response-url.js} +0 -1
  39. package/lib/webhook.d.ts +14 -0
  40. package/lib/webhook.js +69 -0
  41. package/package.json +41 -20
  42. package/plugin.ts +13 -0
  43. package/schema.json +59 -0
  44. package/src/endpoint.ts +322 -227
  45. package/src/index.ts +62 -66
  46. package/src/markdown-to-mrkdwn.ts +63 -4
  47. package/src/platform-permit.ts +1 -1
  48. package/src/protocol.ts +380 -0
  49. package/src/slack-agent-deps.ts +46 -11
  50. package/src/slack-inbound-filter.ts +1 -1
  51. package/src/slack-outbound.ts +32 -134
  52. package/src/webhook.ts +103 -0
  53. package/lib/agent/tools/add_reaction.js +0 -21
  54. package/lib/agent/tools/add_reaction.js.map +0 -1
  55. package/lib/agent/tools/archive_channel.js +0 -21
  56. package/lib/agent/tools/archive_channel.js.map +0 -1
  57. package/lib/agent/tools/edit_message.js +0 -22
  58. package/lib/agent/tools/edit_message.js.map +0 -1
  59. package/lib/agent/tools/invite_to_channel.js +0 -22
  60. package/lib/agent/tools/invite_to_channel.js.map +0 -1
  61. package/lib/agent/tools/pin_message.js +0 -22
  62. package/lib/agent/tools/pin_message.js.map +0 -1
  63. package/lib/agent/tools/remove_reaction.js +0 -21
  64. package/lib/agent/tools/remove_reaction.js.map +0 -1
  65. package/lib/agent/tools/set_purpose.js +0 -22
  66. package/lib/agent/tools/set_purpose.js.map +0 -1
  67. package/lib/agent/tools/set_topic.js +0 -22
  68. package/lib/agent/tools/set_topic.js.map +0 -1
  69. package/lib/agent/tools/unarchive.js +0 -21
  70. package/lib/agent/tools/unarchive.js.map +0 -1
  71. package/lib/agent/tools/unpin_message.js +0 -22
  72. package/lib/agent/tools/unpin_message.js.map +0 -1
  73. package/lib/agent/tools/user_info.js +0 -28
  74. package/lib/agent/tools/user_info.js.map +0 -1
  75. package/lib/src/adapter.js +0 -62
  76. package/lib/src/adapter.js.map +0 -1
  77. package/lib/src/endpoint.js +0 -312
  78. package/lib/src/endpoint.js.map +0 -1
  79. package/lib/src/event-dispatcher.js +0 -233
  80. package/lib/src/event-dispatcher.js.map +0 -1
  81. package/lib/src/index.js +0 -51
  82. package/lib/src/index.js.map +0 -1
  83. package/lib/src/markdown-to-mrkdwn.js +0 -61
  84. package/lib/src/markdown-to-mrkdwn.js.map +0 -1
  85. package/lib/src/mrkdwn-to-markdown.js.map +0 -1
  86. package/lib/src/platform-permit.js.map +0 -1
  87. package/lib/src/segment-mapper.js +0 -2
  88. package/lib/src/segment-mapper.js.map +0 -1
  89. package/lib/src/signing.js +0 -21
  90. package/lib/src/signing.js.map +0 -1
  91. package/lib/src/slack-agent-deps.js +0 -10
  92. package/lib/src/slack-agent-deps.js.map +0 -1
  93. package/lib/src/slack-inbound-filter.js.map +0 -1
  94. package/lib/src/slack-inbound.js +0 -127
  95. package/lib/src/slack-inbound.js.map +0 -1
  96. package/lib/src/slack-message-ref.js.map +0 -1
  97. package/lib/src/slack-outbound.js +0 -223
  98. package/lib/src/slack-outbound.js.map +0 -1
  99. package/lib/src/slack-reaction.js.map +0 -1
  100. package/lib/src/slack-response-url.js.map +0 -1
  101. package/lib/src/slack-side-events.js +0 -64
  102. package/lib/src/slack-side-events.js.map +0 -1
  103. package/lib/src/transport-http.js +0 -83
  104. package/lib/src/transport-http.js.map +0 -1
  105. package/lib/src/transport-socket.js +0 -68
  106. package/lib/src/transport-socket.js.map +0 -1
  107. package/lib/src/types.js +0 -5
  108. package/lib/src/types.js.map +0 -1
  109. package/plugin.yml +0 -3
  110. package/src/adapter.ts +0 -75
  111. package/src/event-dispatcher.ts +0 -255
  112. package/src/segment-mapper.ts +0 -1
  113. package/src/signing.ts +0 -28
  114. package/src/slack-inbound.ts +0 -140
  115. package/src/slack-side-events.ts +0 -74
  116. package/src/transport-http.ts +0 -95
  117. package/src/transport-socket.ts +0 -77
  118. package/src/types.ts +0 -113
package/src/endpoint.ts CHANGED
@@ -1,183 +1,299 @@
1
1
  /**
2
- * Slack Endpoint — 双传输(Socket Mode / HTTP)
2
+ * SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
3
3
  */
4
+ import { SocketModeClient } from '@slack/socket-mode';
4
5
  import { WebClient } from '@slack/web-api';
5
- import { formatCompact, Endpoint, Message, segment, type SendOptions, type EditMessageOptions, expandInteractiveSegmentsInContent } from 'zhin.js';
6
- import type { SlackEndpointConfig, SlackMessageEvent } from './types.js';
7
- import type { SlackAdapter } from './adapter.js';
8
- import { toCanonicalSegments, fromCanonicalSegments } from './segment-mapper.js';
9
- import { parseSlackMessageToSegments, resolveSlackChannelType } from './slack-inbound.js';
10
- import { sendSlackContent, editSlackContent } from './slack-outbound.js';
11
- import { normalizeSlackReactionName } from './slack-reaction.js';
6
+ import type { EndpointInstance } from '@zhin.js/adapter';
7
+ import type { MessageGateway } from '@zhin.js/core/runtime';
8
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
9
+ import { formatCompact, getLogger } from '@zhin.js/logger';
10
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
11
+ import {
12
+ formatInboundContent,
13
+ formatInteractionContent,
14
+ formatSlashContent,
15
+ inboundMessageId,
16
+ resolveSlackChannelType,
17
+ type ResolvedSlackConfig,
18
+ type SlackEvent,
19
+ type SlackEventEnvelope,
20
+ type SlackInteractionPayload,
21
+ type SlackMessageEvent,
22
+ type SlackSlashCommand,
23
+ } from './protocol.js';
24
+ import { registerSlackAgentEndpoint } from './slack-agent-deps.js';
25
+ import {
26
+ createSlackInboundFilterState,
27
+ shouldDropSlackInboundMessage,
28
+ } from './slack-inbound-filter.js';
12
29
  import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
13
- import { SlackEventDispatcher } from './event-dispatcher.js';
14
- import { SlackSocketTransport } from './transport-socket.js';
15
- import { SlackHttpTransport, type RouterLike } from './transport-http.js';
30
+ import { normalizeSlackReactionName } from './slack-reaction.js';
31
+ import { postSlackEphemeral } from './slack-response-url.js';
32
+ import { editSlackContent, sendSlackContent, type SlackChatClient } from './slack-outbound.js';
33
+ import { registerSlackWebhookRoutes, type SlackWebhookHandler } from './webhook.js';
34
+
35
+ const logger = getLogger('slack');
36
+
37
+ export interface SlackSocketLike {
38
+ on(
39
+ event: string,
40
+ handler: (args: { ack: () => Promise<void>; body: unknown }) => void | Promise<void>,
41
+ ): void;
42
+ start(): Promise<void>;
43
+ disconnect(): Promise<void>;
44
+ }
45
+
46
+ export interface SlackWebClientLike extends SlackChatClient {
47
+ auth: {
48
+ test(): Promise<{ user_id?: string; user?: string }>;
49
+ };
50
+ conversations: {
51
+ invite(opts: { channel: string; users: string }): Promise<unknown>;
52
+ kick(opts: { channel: string; user: string }): Promise<unknown>;
53
+ setTopic(opts: { channel: string; topic: string }): Promise<unknown>;
54
+ setPurpose(opts: { channel: string; purpose: string }): Promise<unknown>;
55
+ archive(opts: { channel: string }): Promise<unknown>;
56
+ unarchive(opts: { channel: string }): Promise<unknown>;
57
+ rename(opts: { channel: string; name: string }): Promise<unknown>;
58
+ members(opts: { channel: string }): Promise<{ members?: string[] }>;
59
+ info(opts: { channel: string }): Promise<{ channel?: unknown }>;
60
+ };
61
+ users: {
62
+ info(opts: { user: string }): Promise<{ user?: unknown }>;
63
+ };
64
+ reactions: {
65
+ add(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
66
+ remove(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
67
+ };
68
+ pins: {
69
+ add(opts: { channel: string; timestamp: string }): Promise<unknown>;
70
+ remove(opts: { channel: string; timestamp: string }): Promise<unknown>;
71
+ };
72
+ chat: SlackChatClient['chat'] & {
73
+ delete(opts: { channel: string; ts: string }): Promise<unknown>;
74
+ };
75
+ }
16
76
 
17
- export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessageEvent> {
18
- $connected: boolean;
19
- /** Slack Bot User ID(auth.test.user_id),用于 @ 触发 AI */
20
- $platformUserId?: string;
21
- client?: WebClient;
22
- /** message ts → channel id(Activity Feedback reaction / recall 定位频道) */
23
- readonly messageChannelMap = new Map<string, string>();
24
- senderPermitCache = new Map<string, { at: number; role?: string; permissions: string[] }>();
77
+ export interface SlackEndpointOptions {
78
+ readonly id: CapabilityId;
79
+ readonly gateway: MessageGateway;
80
+ readonly config: ResolvedSlackConfig;
81
+ readonly http?: HttpHost;
82
+ readonly createClient?: (token: string) => SlackWebClientLike;
83
+ readonly createSocket?: (opts: {
84
+ readonly appToken: string;
85
+ readonly clientPingTimeout: number;
86
+ }) => SlackSocketLike;
87
+ }
25
88
 
26
- private dispatcher?: SlackEventDispatcher;
27
- private socketTransport?: SlackSocketTransport;
28
- private httpTransport?: SlackHttpTransport;
89
+ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
90
+ readonly #options: SlackEndpointOptions;
91
+ readonly #inboundFilter = createSlackInboundFilterState();
92
+ readonly #messageChannelMap = new Map<string, string>();
93
+ #client?: SlackWebClientLike;
94
+ #socket?: SlackSocketLike;
95
+ #routeReleases: HttpRouteRegistration[] = [];
96
+ #botUserId?: string;
97
+ #open = false;
98
+ #started = false;
99
+ #unregisterAgent?: () => void;
100
+
101
+ constructor(options: SlackEndpointOptions) {
102
+ this.#options = options;
103
+ }
29
104
 
30
- get logger() { return this.adapter.plugin.logger; }
31
- get $id() { return this.$config.name; }
105
+ /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
106
+ get name(): string {
107
+ return this.#options.config.name;
108
+ }
32
109
 
33
- constructor(public adapter: SlackAdapter, public $config: SlackEndpointConfig) {
34
- this.$connected = false;
110
+ get client(): SlackWebClientLike | undefined {
111
+ return this.#client;
35
112
  }
36
113
 
37
- async $connect(router?: RouterLike): Promise<void> {
114
+ get platformUserId(): string | undefined {
115
+ return this.#botUserId;
116
+ }
117
+
118
+ get config(): ResolvedSlackConfig {
119
+ return this.#options.config;
120
+ }
121
+
122
+ async start(): Promise<void> {
123
+ if (this.#started) return;
124
+ this.#started = true;
38
125
  try {
39
- this.client = new WebClient(this.$config.token);
40
- this.dispatcher = new SlackEventDispatcher(this);
41
-
42
- if (this.$config.socketMode) {
43
- this.socketTransport = new SlackSocketTransport(this.$config, this.dispatcher, this.logger);
44
- await this.socketTransport.connect();
45
- } else if (router) {
46
- this.httpTransport = new SlackHttpTransport(this.$config, this.dispatcher, this.logger);
47
- this.httpTransport.registerRoutes(router);
126
+ const { config } = this.#options;
127
+ this.#client = this.#options.createClient?.(config.token)
128
+ ?? (new WebClient(config.token) as unknown as SlackWebClientLike);
129
+
130
+ this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
131
+
132
+ if (config.mode === 'socket') {
133
+ await this.#startSocket();
134
+ } else {
135
+ const http = this.#options.http;
136
+ if (!http) {
137
+ throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
138
+ }
139
+ this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
140
+ logger.debug(formatCompact({
141
+ endpoint: config.name,
142
+ op: 'webhook',
143
+ path: config.webhookPath,
144
+ }));
48
145
  }
49
146
 
50
- this.$connected = true;
147
+ const authTest = await this.#client.auth.test();
148
+ if (authTest.user_id) this.#botUserId = String(authTest.user_id);
51
149
 
52
- const authTest = await this.client.auth.test();
53
- if (authTest.user_id) {
54
- this.$platformUserId = String(authTest.user_id);
55
- }
56
- this.logger.debug(formatCompact({
57
- endpoint: this.$config.name,
58
- user: authTest.user,
59
- platform_user_id: this.$platformUserId,
150
+ logger.info(formatCompact({
151
+ op: 'connect',
152
+ endpoint: config.name,
153
+ mode: config.mode,
154
+ platform_user_id: this.#botUserId,
60
155
  }));
61
-
62
- if (!this.$config.socketMode && router) {
63
- this.logger.debug(formatCompact({ op: 'listen', mode: 'http', path: '/slack/events' }));
64
- }
65
156
  } catch (error) {
66
- this.logger.error('Failed to connect Slack bot:', error);
67
- this.$connected = false;
157
+ await this.stop();
158
+ logger.error('Failed to connect Slack endpoint:', error);
68
159
  throw error;
69
160
  }
70
161
  }
71
162
 
72
- async $disconnect(): Promise<void> {
73
- try {
74
- if (this.socketTransport) {
75
- await this.socketTransport.disconnect();
76
- this.socketTransport = undefined;
163
+ open(): void {
164
+ this.#open = true;
165
+ }
166
+
167
+ close(): void {
168
+ this.#open = false;
169
+ }
170
+
171
+ async stop(): Promise<void> {
172
+ this.#open = false;
173
+ if (this.#socket) {
174
+ try {
175
+ await this.#socket.disconnect();
176
+ } catch {
177
+ /* ignore */
77
178
  }
78
- this.httpTransport = undefined;
79
- this.$connected = false;
80
- this.logger.debug(formatCompact({ op: 'disconnect', endpoint: this.$config.name }));
81
- } catch (error) {
82
- this.logger.error('Error disconnecting Slack bot:', error);
83
- throw error;
179
+ this.#socket = undefined;
84
180
  }
181
+ for (const release of this.#routeReleases.splice(0)) release();
182
+ this.#unregisterAgent?.();
183
+ this.#unregisterAgent = undefined;
184
+ this.#client = undefined;
185
+ this.#started = false;
186
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
85
187
  }
86
188
 
87
- $formatMessage(msg: SlackMessageEvent): Message<SlackMessageEvent> {
88
- const channelType = resolveSlackChannelType(msg);
89
- const channelId = msg.channel;
90
- this.trackMessageChannel(msg.ts, channelId);
91
- const wire = parseSlackMessageToSegments(msg);
92
- const content = toCanonicalSegments(wire);
93
- const userId = msg.user ?? '';
94
- const userName = (msg as any).username ?? (userId || 'Unknown');
95
- const messageText = msg.text ?? '';
96
-
97
- this.trackMessageChannel(msg.ts, channelId);
98
-
99
- const result = Message.from(msg, {
100
- $id: formatSlackMessageRef(channelId, msg.ts),
101
- $adapter: 'slack',
102
- $endpoint: this.$config.name,
103
- $sender: { id: userId, name: userName },
104
- $channel: { id: channelId, type: channelType },
105
- $content: content,
106
- $raw: messageText,
107
- $timestamp: parseFloat(msg.ts) * 1000,
108
- $quote_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
109
- $recall: async () => {
110
- await this.client!.chat.delete({ channel: channelId, ts: msg.ts });
111
- },
112
- $reply: async (replyContent, quote?) => {
113
- if (!Array.isArray(replyContent)) replyContent = [replyContent];
114
- const threadTs = quote
115
- ? (typeof quote === 'boolean' ? msg.ts : quote)
116
- : (msg.thread_ts ?? undefined);
117
- return await this.adapter.sendMessage({
118
- context: 'slack',
119
- endpoint: this.$config.name,
120
- id: channelId,
121
- type: channelType,
122
- content: replyContent,
123
- ...(threadTs ? { threadId: threadTs } : {}),
124
- });
125
- },
126
- });
189
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
190
+ if (!this.#client) throw new Error('Slack client not connected');
191
+ const { channel, threadTs } = parseSendTarget(target);
192
+ const result = await sendSlackContent(
193
+ this.#client,
194
+ payload,
195
+ { channel, threadTs },
196
+ logger,
197
+ );
198
+ if (result.ts) this.trackMessageChannel(result.ts, channel);
199
+ return formatSlackMessageRef(channel, result.ts || String(Date.now()));
200
+ }
127
201
 
128
- return result;
202
+ /** Test / internal: admit a message event when open. */
203
+ admit(event: SlackMessageEvent | SlackEvent): void {
204
+ if (!this.#open) return;
205
+ if (event.type !== 'message' && event.type !== 'app_mention') return;
206
+ const msg = event as SlackMessageEvent;
207
+ if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
208
+ if (!msg.channel || !msg.ts) return;
209
+
210
+ this.trackMessageChannel(msg.ts, msg.channel);
211
+ void this.#options.gateway.receive({
212
+ adapter: this.#options.id,
213
+ target: msg.channel,
214
+ content: formatInboundContent(msg),
215
+ sender: msg.user ?? msg.channel,
216
+ id: inboundMessageId(msg),
217
+ metadata: Object.freeze({
218
+ endpoint: this.#options.config.name,
219
+ channelType: resolveSlackChannelType(msg),
220
+ userId: msg.user,
221
+ threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
222
+ ts: msg.ts,
223
+ // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
224
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
225
+ }),
226
+ }).catch((err) => {
227
+ logger.warn(formatCompact({
228
+ op: 'slack_gateway_receive_failed',
229
+ target: msg.channel,
230
+ error: err instanceof Error ? err.message : String(err),
231
+ }));
232
+ });
129
233
  }
130
234
 
131
- async $sendMessage(options: SendOptions): Promise<string> {
132
- try {
133
- const canonical = expandInteractiveSegmentsInContent(options.content);
134
- const wire = fromCanonicalSegments(canonical);
135
- const result = await sendSlackContent(this.client!, wire, {
136
- channel: options.id,
137
- threadTs: options.threadId,
138
- }, this.logger);
139
- this.logger.debug(
140
- `${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`,
141
- );
142
- this.trackMessageChannel(result.ts, options.id);
143
- return formatSlackMessageRef(options.id, result.ts);
144
- } catch (error) {
145
- this.logger.error('Failed to send Slack message:', error);
146
- throw error;
235
+ admitInteraction(payload: SlackInteractionPayload): void {
236
+ if (!this.#open) return;
237
+ if (payload.type !== 'block_actions' || !payload.actions?.length) return;
238
+ const channelId = payload.channel?.id ?? '';
239
+ const userId = payload.user.id;
240
+ const messageTs = payload.message?.ts ?? '';
241
+ if (payload.response_url) {
242
+ postSlackEphemeral(payload.response_url, '已收到', logger);
147
243
  }
244
+ void this.#options.gateway.receive({
245
+ adapter: this.#options.id,
246
+ target: channelId || userId,
247
+ content: formatInteractionContent(payload),
248
+ sender: userId,
249
+ id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
250
+ metadata: Object.freeze({
251
+ endpoint: this.#options.config.name,
252
+ eventType: 'block_actions',
253
+ actionId: payload.actions[0]?.action_id,
254
+ threadTs: messageTs || undefined,
255
+ }),
256
+ }).catch((err) => {
257
+ logger.warn(formatCompact({
258
+ op: 'slack_gateway_receive_failed',
259
+ target: channelId,
260
+ error: err instanceof Error ? err.message : String(err),
261
+ }));
262
+ });
148
263
  }
149
264
 
150
- async $editMessage(options: EditMessageOptions): Promise<void> {
151
- const ref = parseSlackMessageRef(options.messageId);
152
- const channel = ref?.channel ?? options.id;
153
- const ts = ref?.ts ?? options.messageId;
154
- const canonical = expandInteractiveSegmentsInContent(options.content);
155
- const wire = fromCanonicalSegments(canonical);
156
- await editSlackContent(this.client!, channel, ts, wire);
265
+ admitSlashCommand(cmd: SlackSlashCommand): void {
266
+ if (!this.#open) return;
267
+ postSlackEphemeral(cmd.response_url, '处理中…', logger);
268
+ void this.#options.gateway.receive({
269
+ adapter: this.#options.id,
270
+ target: cmd.channel_id,
271
+ content: formatSlashContent(cmd),
272
+ sender: cmd.user_id,
273
+ id: cmd.trigger_id,
274
+ metadata: Object.freeze({
275
+ endpoint: this.#options.config.name,
276
+ eventType: 'slash_command',
277
+ command: cmd.command,
278
+ }),
279
+ }).catch((err) => {
280
+ logger.warn(formatCompact({
281
+ op: 'slack_gateway_receive_failed',
282
+ target: cmd.channel_id,
283
+ error: err instanceof Error ? err.message : String(err),
284
+ }));
285
+ });
157
286
  }
158
287
 
159
- async $recallMessage(id: string): Promise<void> {
160
- const ref = this.resolveMessageRef(id);
161
- if (!ref) {
162
- this.logger.warn(formatCompact({ op: 'recall_skip', reason: 'channel_unknown', message_id: id }));
163
- return;
164
- }
165
- try {
166
- await this.client!.chat.delete({ channel: ref.channel, ts: ref.ts });
167
- } catch (error: unknown) {
168
- const slackError = (error as { data?: { error?: string } })?.data?.error;
169
- if (slackError === 'message_not_found') {
170
- this.logger.debug(formatCompact({ op: 'recall_skip', reason: 'message_not_found', message_id: id }));
171
- return;
172
- }
173
- throw error;
288
+ handleEnvelope(body: unknown): void {
289
+ const envelope = body as SlackEventEnvelope;
290
+ if (envelope?.type === 'event_callback' && envelope.event) {
291
+ this.admit(envelope.event);
174
292
  }
175
293
  }
176
294
 
177
295
  trackMessageChannel(ts: string, channel: string): void {
178
- if (ts && channel) {
179
- this.messageChannelMap.set(ts, channel);
180
- }
296
+ if (ts && channel) this.#messageChannelMap.set(ts, channel);
181
297
  }
182
298
 
183
299
  resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
@@ -186,155 +302,134 @@ export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessage
186
302
  this.trackMessageChannel(parsed.ts, parsed.channel);
187
303
  return parsed;
188
304
  }
189
- if (channelHint) {
190
- return { channel: channelHint, ts: messageId };
191
- }
192
- const channel = this.messageChannelMap.get(messageId);
193
- if (channel) {
194
- return { channel, ts: messageId };
195
- }
196
- return null;
197
- }
198
-
199
- /**
200
- * Activity Feedback:在用户消息上添加表情回应
201
- * @returns reaction name,供 $removeReaction 使用
202
- */
203
- async $addReaction(
204
- messageId: string,
205
- emoji: string,
206
- hint?: { sceneType?: 'private' | 'group' | 'channel'; channelId?: string },
207
- ): Promise<string | null> {
208
- const ref = this.resolveMessageRef(messageId, hint?.channelId);
209
- if (!ref) {
210
- this.logger.warn(formatCompact({ op: 'reaction_add_skip', reason: 'channel_unknown', message_id: messageId }));
211
- return null;
212
- }
213
- const name = normalizeSlackReactionName(emoji);
214
- try {
215
- await this.addReaction(ref.channel, ref.ts, name);
216
- return name;
217
- } catch (error) {
218
- this.logger.error('Failed to add Slack reaction:', error);
219
- return null;
220
- }
305
+ if (channelHint) return { channel: channelHint, ts: messageId };
306
+ const channel = this.#messageChannelMap.get(messageId);
307
+ return channel ? { channel, ts: messageId } : null;
221
308
  }
222
309
 
223
- /** Activity Feedback:移除本 Bot 在消息上的表情回应 */
224
- async $removeReaction(messageId: string, reactionId: string, channelHint?: string): Promise<void> {
225
- const ref = this.resolveMessageRef(messageId, channelHint);
226
- if (!ref) {
227
- this.logger.warn(formatCompact({ op: 'reaction_remove_skip', reason: 'channel_unknown', message_id: messageId }));
228
- return;
229
- }
230
- const name = normalizeSlackReactionName(reactionId);
231
- try {
232
- await this.removeReaction(ref.channel, ref.ts, name);
233
- } catch (error) {
234
- const code = (error as { data?: { error?: string } })?.data?.error;
235
- if (code === 'no_reaction') return;
236
- this.logger.error('Failed to remove Slack reaction:', error);
237
- }
310
+ async editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
311
+ if (!this.#client) throw new Error('Slack client not connected');
312
+ await editSlackContent(this.#client, channel, messageTs, content);
238
313
  }
239
314
 
240
- // ==================== 工作区管理 API ====================
315
+ // ── Agent tool surface ──────────────────────────────────────────────
241
316
 
242
317
  async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
243
- await this.client!.conversations.invite({ channel, users: users.join(',') });
244
- this.logger.debug(formatCompact({ op: 'invite', endpoint: this.$id, channel, users: users.join(',') }));
318
+ await this.#client!.conversations.invite({ channel, users: users.join(',') });
245
319
  return true;
246
320
  }
247
321
 
248
322
  async kickFromChannel(channel: string, user: string): Promise<boolean> {
249
- await this.client!.conversations.kick({ channel, user });
250
- this.logger.debug(formatCompact({ op: 'kick', endpoint: this.$id, channel, user }));
323
+ await this.#client!.conversations.kick({ channel, user });
251
324
  return true;
252
325
  }
253
326
 
254
327
  async setChannelTopic(channel: string, topic: string): Promise<boolean> {
255
- await this.client!.conversations.setTopic({ channel, topic });
256
- this.logger.debug(formatCompact({ op: 'set_topic', endpoint: this.$id, channel }));
328
+ await this.#client!.conversations.setTopic({ channel, topic });
257
329
  return true;
258
330
  }
259
331
 
260
332
  async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
261
- await this.client!.conversations.setPurpose({ channel, purpose });
262
- this.logger.debug(formatCompact({ op: 'set_purpose', endpoint: this.$id, channel }));
333
+ await this.#client!.conversations.setPurpose({ channel, purpose });
263
334
  return true;
264
335
  }
265
336
 
266
337
  async archiveChannel(channel: string): Promise<boolean> {
267
- await this.client!.conversations.archive({ channel });
268
- this.logger.debug(formatCompact({ op: 'archive', endpoint: this.$id, channel }));
338
+ await this.#client!.conversations.archive({ channel });
269
339
  return true;
270
340
  }
271
341
 
272
342
  async unarchiveChannel(channel: string): Promise<boolean> {
273
- await this.client!.conversations.unarchive({ channel });
274
- this.logger.debug(formatCompact({ op: 'unarchive', endpoint: this.$id, channel }));
343
+ await this.#client!.conversations.unarchive({ channel });
275
344
  return true;
276
345
  }
277
346
 
278
347
  async renameChannel(channel: string, name: string): Promise<boolean> {
279
- await this.client!.conversations.rename({ channel, name });
280
- this.logger.debug(formatCompact({ op: 'rename', endpoint: this.$id, channel, name }));
348
+ await this.#client!.conversations.rename({ channel, name });
281
349
  return true;
282
350
  }
283
351
 
284
352
  async getChannelMembers(channel: string): Promise<string[]> {
285
- const result = await this.client!.conversations.members({ channel });
353
+ const result = await this.#client!.conversations.members({ channel });
286
354
  return result.members || [];
287
355
  }
288
356
 
289
- async getChannelInfo(channel: string): Promise<any> {
290
- const result = await this.client!.conversations.info({ channel });
357
+ async getChannelInfo(channel: string): Promise<unknown> {
358
+ const result = await this.#client!.conversations.info({ channel });
291
359
  return result.channel;
292
360
  }
293
361
 
294
- async getUserInfo(user: string): Promise<any> {
295
- const result = await this.client!.users.info({ user });
362
+ async getUserInfo(user: string): Promise<unknown> {
363
+ const result = await this.#client!.users.info({ user });
296
364
  return result.user;
297
365
  }
298
366
 
299
367
  async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
368
+ const reaction = normalizeSlackReactionName(name);
300
369
  try {
301
- await this.client!.reactions.add({ channel, timestamp, name });
302
- this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name }));
370
+ await this.#client!.reactions.add({ channel, timestamp, name: reaction });
303
371
  return true;
304
372
  } catch (error) {
305
373
  const code = (error as { data?: { error?: string } })?.data?.error;
306
- if (code === 'already_reacted') {
307
- this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name, status: 'exists' }));
308
- return true;
309
- }
374
+ if (code === 'already_reacted') return true;
310
375
  throw error;
311
376
  }
312
377
  }
313
378
 
314
379
  async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
380
+ const reaction = normalizeSlackReactionName(name);
315
381
  try {
316
- await this.client!.reactions.remove({ channel, timestamp, name });
317
- this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name }));
382
+ await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
318
383
  return true;
319
384
  } catch (error) {
320
385
  const code = (error as { data?: { error?: string } })?.data?.error;
321
- if (code === 'no_reaction') {
322
- this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name, status: 'gone' }));
323
- return true;
324
- }
386
+ if (code === 'no_reaction') return true;
325
387
  throw error;
326
388
  }
327
389
  }
328
390
 
329
391
  async pinMessage(channel: string, timestamp: string): Promise<boolean> {
330
- await this.client!.pins.add({ channel, timestamp });
331
- this.logger.debug(formatCompact({ op: 'pin', endpoint: this.$id, channel }));
392
+ await this.#client!.pins.add({ channel, timestamp });
332
393
  return true;
333
394
  }
334
395
 
335
396
  async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
336
- await this.client!.pins.remove({ channel, timestamp });
337
- this.logger.debug(formatCompact({ op: 'unpin', endpoint: this.$id, channel }));
397
+ await this.#client!.pins.remove({ channel, timestamp });
338
398
  return true;
339
399
  }
400
+
401
+ async #startSocket(): Promise<void> {
402
+ const { config } = this.#options;
403
+ if (!config.appToken) throw new Error('Socket Mode requires appToken');
404
+ this.#socket = this.#options.createSocket?.({
405
+ appToken: config.appToken,
406
+ clientPingTimeout: config.clientPingTimeout,
407
+ }) ?? new SocketModeClient({
408
+ appToken: config.appToken,
409
+ clientPingTimeout: config.clientPingTimeout,
410
+ }) as unknown as SlackSocketLike;
411
+
412
+ this.#socket.on('slack_event', async ({ ack, body }) => {
413
+ await ack();
414
+ this.handleEnvelope(body);
415
+ });
416
+ this.#socket.on('interactive', async ({ ack, body }) => {
417
+ await ack();
418
+ this.admitInteraction(body as SlackInteractionPayload);
419
+ });
420
+ this.#socket.on('slash_commands', async ({ ack, body }) => {
421
+ await ack();
422
+ this.admitSlashCommand(body as SlackSlashCommand);
423
+ });
424
+
425
+ await this.#socket.start();
426
+ }
427
+ }
428
+
429
+ function parseSendTarget(target: string): { channel: string; threadTs?: string } {
430
+ const parsed = parseSlackMessageRef(target);
431
+ if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
432
+ return { channel: parsed.channel, threadTs: parsed.ts };
433
+ }
434
+ return { channel: target };
340
435
  }