@zhin.js/adapter-slack 4.0.1 → 4.1.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +77 -160
  3. package/adapters/slack.ts +34 -0
  4. package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
  5. package/agent/tools/add_reaction.ts +26 -0
  6. package/agent/tools/archive_channel.ts +24 -0
  7. package/agent/tools/edit_message.ts +28 -0
  8. package/agent/tools/invite_to_channel.ts +26 -0
  9. package/agent/tools/pin_message.ts +26 -0
  10. package/agent/tools/remove_reaction.ts +26 -0
  11. package/agent/tools/set_purpose.ts +26 -0
  12. package/agent/tools/set_topic.ts +26 -0
  13. package/agent/tools/unarchive.ts +24 -0
  14. package/agent/tools/unpin_message.ts +26 -0
  15. package/agent/tools/user_info.ts +31 -0
  16. package/lib/endpoint.d.ts +133 -94
  17. package/lib/endpoint.js +270 -612
  18. package/lib/index.d.ts +11 -10
  19. package/lib/index.js +11 -252
  20. package/lib/markdown-to-mrkdwn.d.ts +9 -0
  21. package/lib/markdown-to-mrkdwn.js +121 -0
  22. package/lib/mrkdwn-to-markdown.d.ts +4 -0
  23. package/lib/mrkdwn-to-markdown.js +30 -0
  24. package/lib/platform-permit.d.ts +1 -2
  25. package/lib/platform-permit.js +4 -2
  26. package/lib/protocol.d.ts +143 -0
  27. package/lib/protocol.js +225 -0
  28. package/lib/slack-agent-deps.d.ts +28 -0
  29. package/lib/slack-agent-deps.js +30 -0
  30. package/lib/slack-inbound-filter.d.ts +12 -0
  31. package/lib/slack-inbound-filter.js +46 -0
  32. package/lib/slack-message-ref.d.ts +7 -0
  33. package/lib/slack-message-ref.js +17 -0
  34. package/lib/slack-outbound.d.ts +24 -0
  35. package/lib/slack-outbound.js +109 -0
  36. package/lib/slack-reaction.d.ts +1 -0
  37. package/lib/slack-reaction.js +26 -0
  38. package/lib/slack-response-url.d.ts +5 -0
  39. package/lib/slack-response-url.js +16 -0
  40. package/lib/webhook.d.ts +14 -0
  41. package/lib/webhook.js +69 -0
  42. package/package.json +53 -14
  43. package/plugin.ts +13 -0
  44. package/schema.json +38 -0
  45. package/src/endpoint.ts +339 -647
  46. package/src/index.ts +65 -277
  47. package/src/markdown-to-mrkdwn.ts +117 -0
  48. package/src/mrkdwn-to-markdown.ts +29 -0
  49. package/src/platform-permit.ts +1 -2
  50. package/src/protocol.ts +380 -0
  51. package/src/slack-agent-deps.ts +57 -0
  52. package/src/slack-inbound-filter.ts +60 -0
  53. package/src/slack-message-ref.ts +18 -0
  54. package/src/slack-outbound.ts +167 -0
  55. package/src/slack-reaction.ts +23 -0
  56. package/src/slack-response-url.ts +26 -0
  57. package/src/webhook.ts +103 -0
  58. package/lib/adapter.d.ts +0 -19
  59. package/lib/adapter.d.ts.map +0 -1
  60. package/lib/adapter.js +0 -46
  61. package/lib/adapter.js.map +0 -1
  62. package/lib/endpoint.d.ts.map +0 -1
  63. package/lib/endpoint.js.map +0 -1
  64. package/lib/index.d.ts.map +0 -1
  65. package/lib/index.js.map +0 -1
  66. package/lib/platform-permit.d.ts.map +0 -1
  67. package/lib/platform-permit.js.map +0 -1
  68. package/lib/segment-mapper.d.ts +0 -2
  69. package/lib/segment-mapper.d.ts.map +0 -1
  70. package/lib/segment-mapper.js +0 -2
  71. package/lib/segment-mapper.js.map +0 -1
  72. package/lib/types.d.ts +0 -17
  73. package/lib/types.d.ts.map +0 -1
  74. package/lib/types.js +0 -2
  75. package/lib/types.js.map +0 -1
  76. package/plugin.yml +0 -3
  77. package/src/adapter.ts +0 -54
  78. package/src/segment-mapper.ts +0 -1
  79. package/src/types.ts +0 -18
  80. /package/{skills/slack → agent}/PERMITS.md +0 -0
package/lib/endpoint.js CHANGED
@@ -1,677 +1,335 @@
1
1
  /**
2
- * Slack Endpoint 实现
2
+ * SlackEndpoint lifecycle, outbound, admit, Socket Mode, agent tool surface.
3
3
  */
4
- import { App as SlackApp, LogLevel } from "@slack/bolt";
5
- import { WebClient } from "@slack/web-api";
6
- import { formatCompact, Message, segment, expandInteractiveSegmentsInContent, } from 'zhin.js';
7
- import { normalizeSlackSenderForPermit } from "./platform-permit.js";
8
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
4
+ import { SocketModeClient } from '@slack/socket-mode';
5
+ import { WebClient } from '@slack/web-api';
6
+ import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import { formatInboundContent, formatInteractionContent, formatSlashContent, inboundMessageId, resolveSlackChannelType, } from './protocol.js';
8
+ import { registerSlackAgentEndpoint } from './slack-agent-deps.js';
9
+ import { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
10
+ import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
11
+ import { normalizeSlackReactionName } from './slack-reaction.js';
12
+ import { postSlackEphemeral } from './slack-response-url.js';
13
+ import { editSlackContent, sendSlackContent } from './slack-outbound.js';
14
+ import { registerSlackWebhookRoutes } from './webhook.js';
15
+ const logger = getLogger('slack');
9
16
  export class SlackEndpoint {
10
- adapter;
11
- $config;
12
- $connected;
13
- /** 延迟到 `$connect`,避免子类 mock `$connect` 时仍在构造函数里创建 Bolt/WebClient(会在后台触发 Slack API) */
14
- app;
15
- client;
16
- get logger() {
17
- return this.adapter.plugin.logger;
18
- }
19
- get $id() {
20
- return this.$config.name;
21
- }
22
- constructor(adapter, $config) {
23
- this.adapter = adapter;
24
- this.$config = $config;
25
- this.$connected = false;
26
- }
27
- #ensureSlackRuntime() {
28
- if (this.app && this.client)
29
- return;
30
- const $config = this.$config;
31
- if ($config.socketMode && $config.appToken) {
32
- this.app = new SlackApp({
33
- token: $config.token,
34
- signingSecret: $config.signingSecret,
35
- appToken: $config.appToken,
36
- socketMode: true,
37
- logLevel: $config.logLevel || LogLevel.INFO,
38
- });
39
- }
40
- else {
41
- this.app = new SlackApp({
42
- token: $config.token,
43
- signingSecret: $config.signingSecret,
44
- socketMode: false,
45
- logLevel: $config.logLevel || LogLevel.INFO,
46
- });
47
- }
48
- this.client = new WebClient($config.token);
17
+ #options;
18
+ #inboundFilter = createSlackInboundFilterState();
19
+ #messageChannelMap = new Map();
20
+ #client;
21
+ #socket;
22
+ #routeReleases = [];
23
+ #botUserId;
24
+ #open = false;
25
+ #started = false;
26
+ #unregisterAgent;
27
+ constructor(options) {
28
+ this.#options = options;
29
+ }
30
+ get client() {
31
+ return this.#client;
32
+ }
33
+ get platformUserId() {
34
+ return this.#botUserId;
49
35
  }
50
- async $connect() {
51
- this.#ensureSlackRuntime();
36
+ get config() {
37
+ return this.#options.config;
38
+ }
39
+ async start() {
40
+ if (this.#started)
41
+ return;
42
+ this.#started = true;
52
43
  try {
53
- // Set up message event handler
54
- this.app.message(async ({ message, say }) => {
55
- await this.handleSlackMessage(message);
56
- });
57
- // Set up app mention handler
58
- this.app.event("app_mention", async ({ event, say }) => {
59
- await this.handleSlackMessage(event);
60
- });
61
- // Start the app
62
- const port = this.$config.port || 3000;
63
- if (this.$config.socketMode) {
64
- await this.app.start();
44
+ const { config } = this.#options;
45
+ this.#client = this.#options.createClient?.(config.token)
46
+ ?? new WebClient(config.token);
47
+ this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
48
+ if (config.mode === 'socket') {
49
+ await this.#startSocket();
65
50
  }
66
51
  else {
67
- await this.app.start(port);
68
- }
69
- this.$connected = true;
70
- // Get bot info
71
- const authTest = await this.client.auth.test();
72
- this.logger.info(formatCompact({ endpoint: this.$config.name, user: authTest.user }));
73
- if (!this.$config.socketMode) {
74
- this.logger.info(formatCompact({ op: "listen", port }));
52
+ const http = this.#options.http;
53
+ if (!http) {
54
+ throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
55
+ }
56
+ this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
57
+ logger.debug(formatCompact({
58
+ endpoint: config.name,
59
+ op: 'webhook',
60
+ path: config.webhookPath,
61
+ }));
75
62
  }
63
+ const authTest = await this.#client.auth.test();
64
+ if (authTest.user_id)
65
+ this.#botUserId = String(authTest.user_id);
66
+ logger.info(formatCompact({
67
+ op: 'connect',
68
+ endpoint: config.name,
69
+ mode: config.mode,
70
+ platform_user_id: this.#botUserId,
71
+ }));
76
72
  }
77
73
  catch (error) {
78
- this.logger.error("Failed to connect Slack bot:", error);
79
- this.$connected = false;
74
+ await this.stop();
75
+ logger.error('Failed to connect Slack endpoint:', error);
80
76
  throw error;
81
77
  }
82
78
  }
83
- async $disconnect() {
84
- if (!this.app) {
85
- this.$connected = false;
86
- return;
87
- }
88
- try {
89
- await this.app.stop();
90
- this.$connected = false;
91
- this.logger.info(formatCompact({ op: "disconnect", endpoint: this.$config.name }));
92
- }
93
- catch (error) {
94
- this.logger.error("Error disconnecting Slack bot:", error);
95
- throw error;
96
- }
79
+ open() {
80
+ this.#open = true;
97
81
  }
98
- senderPermitCache = new Map();
99
- async enrichChannelSender(message, msg) {
100
- if (message.$channel.type !== "group" || !("user" in msg) || !msg.user)
101
- return;
102
- const channelId = msg.channel;
103
- const userId = msg.user;
104
- const key = `${channelId}:${userId}`;
105
- const now = Date.now();
106
- const cached = this.senderPermitCache.get(key);
107
- if (cached && now - cached.at < 60_000) {
108
- message.$sender.role = cached.role;
109
- message.$sender.permissions = cached.permissions;
110
- return;
111
- }
112
- try {
113
- const user = await this.getUserInfo(userId);
114
- let isChannelManager = false;
82
+ close() {
83
+ this.#open = false;
84
+ }
85
+ async stop() {
86
+ this.#open = false;
87
+ if (this.#socket) {
115
88
  try {
116
- const channel = await this.getChannelInfo(channelId);
117
- if (channel?.creator === userId)
118
- isChannelManager = true;
89
+ await this.#socket.disconnect();
119
90
  }
120
91
  catch {
121
- // ignore
92
+ /* ignore */
122
93
  }
123
- const normalized = normalizeSlackSenderForPermit({
124
- isWorkspaceOwner: user.is_owner === true,
125
- isWorkspaceAdmin: user.is_admin === true,
126
- isChannelManager,
127
- });
128
- const entry = {
129
- at: now,
130
- role: normalized.role,
131
- permissions: normalized.permissions ?? [],
132
- };
133
- this.senderPermitCache.set(key, entry);
134
- message.$sender.role = entry.role;
135
- message.$sender.permissions = entry.permissions;
136
- }
137
- catch {
138
- // 保守拒绝
139
- }
94
+ this.#socket = undefined;
95
+ }
96
+ for (const release of this.#routeReleases.splice(0))
97
+ release();
98
+ this.#unregisterAgent?.();
99
+ this.#unregisterAgent = undefined;
100
+ this.#client = undefined;
101
+ this.#started = false;
102
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
140
103
  }
141
- async handleSlackMessage(msg) {
142
- // Ignore bot messages and message changes
143
- if ("subtype" in msg && (msg.subtype === "bot_message" || msg.subtype === "message_changed")) {
104
+ async send({ target, payload }) {
105
+ if (!this.#client)
106
+ throw new Error('Slack client not connected');
107
+ const { channel, threadTs } = parseSendTarget(target);
108
+ const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, logger);
109
+ if (result.ts)
110
+ this.trackMessageChannel(result.ts, channel);
111
+ return formatSlackMessageRef(channel, result.ts || String(Date.now()));
112
+ }
113
+ /** Test / internal: admit a message event when open. */
114
+ admit(event) {
115
+ if (!this.#open)
144
116
  return;
145
- }
146
- const message = this.$formatMessage(msg);
147
- await this.enrichChannelSender(message, msg);
148
- this.adapter.emit("message.receive", message);
149
- this.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
150
- }
151
- $formatMessage(msg) {
152
- // Determine channel type based on channel ID
153
- const channelType = "channel_type" in msg && msg.channel_type === "im" ? "private" : "group";
154
- const channelId = msg.channel;
155
- // Parse message content
156
- const wire = this.parseMessageContent(msg);
157
- const content = toCanonicalSegments(wire);
158
- // Extract user info safely
159
- const userId = ("user" in msg ? msg.user : "") || "";
160
- const userName = ("username" in msg ? msg.username : null) || userId || "Unknown";
161
- const messageText = ("text" in msg ? msg.text : "") || "";
162
- const result = Message.from(msg, {
163
- $id: msg.ts,
164
- $adapter: "slack",
165
- $endpoint: this.$config.name,
166
- $sender: {
167
- id: userId,
168
- name: userName,
169
- },
170
- $channel: {
171
- id: channelId,
172
- type: channelType,
173
- },
174
- $content: content,
175
- $raw: messageText,
176
- $timestamp: parseFloat(msg.ts) * 1000,
177
- $recall: async () => {
178
- try {
179
- await this.client.chat.delete({
180
- channel: channelId,
181
- ts: result.$id,
182
- });
183
- }
184
- catch (error) {
185
- this.logger.error("Error recalling Slack message:", error);
186
- throw error;
187
- }
188
- },
189
- $reply: async (content, quote) => {
190
- if (!Array.isArray(content))
191
- content = [content];
192
- const sendOptions = {
193
- channel: channelId,
194
- };
195
- // Handle thread reply
196
- if (quote) {
197
- const threadTs = typeof quote === "boolean" ? result.$id : quote;
198
- sendOptions.thread_ts = threadTs;
199
- }
200
- return await this.adapter.sendMessage({
201
- context: "slack",
202
- endpoint: this.$config.name,
203
- id: channelId,
204
- type: "channel",
205
- content: content,
206
- });
207
- },
117
+ if (event.type !== 'message' && event.type !== 'app_mention')
118
+ return;
119
+ const msg = event;
120
+ if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
121
+ return;
122
+ if (!msg.channel || !msg.ts)
123
+ return;
124
+ this.trackMessageChannel(msg.ts, msg.channel);
125
+ void this.#options.gateway.receive({
126
+ adapter: this.#options.id,
127
+ target: msg.channel,
128
+ content: formatInboundContent(msg),
129
+ sender: msg.user ?? msg.channel,
130
+ id: inboundMessageId(msg),
131
+ metadata: Object.freeze({
132
+ endpoint: this.#options.config.name,
133
+ channelType: resolveSlackChannelType(msg),
134
+ userId: msg.user,
135
+ threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
136
+ ts: msg.ts,
137
+ // app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
138
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
139
+ }),
140
+ }).catch((err) => {
141
+ logger.warn(formatCompact({
142
+ op: 'slack_gateway_receive_failed',
143
+ target: msg.channel,
144
+ error: err instanceof Error ? err.message : String(err),
145
+ }));
208
146
  });
209
- return result;
210
- }
211
- parseMessageContent(msg) {
212
- const segments = [];
213
- // Handle text
214
- if ("text" in msg && msg.text) {
215
- // Parse Slack formatting
216
- segments.push(...this.parseSlackText(msg.text));
217
- }
218
- // Handle files
219
- if ("files" in msg && msg.files) {
220
- for (const file of msg.files) {
221
- if (file.mimetype?.startsWith("image/")) {
222
- segments.push({
223
- type: "image",
224
- data: {
225
- id: file.id,
226
- name: file.name,
227
- url: file.url_private || file.permalink,
228
- size: file.size,
229
- mimetype: file.mimetype,
230
- },
231
- });
232
- }
233
- else if (file.mimetype?.startsWith("video/")) {
234
- segments.push({
235
- type: "video",
236
- data: {
237
- id: file.id,
238
- name: file.name,
239
- url: file.url_private || file.permalink,
240
- size: file.size,
241
- mimetype: file.mimetype,
242
- },
243
- });
244
- }
245
- else if (file.mimetype?.startsWith("audio/")) {
246
- segments.push({
247
- type: "audio",
248
- data: {
249
- id: file.id,
250
- name: file.name,
251
- url: file.url_private || file.permalink,
252
- size: file.size,
253
- mimetype: file.mimetype,
254
- },
255
- });
256
- }
257
- else {
258
- segments.push({
259
- type: "file",
260
- data: {
261
- id: file.id,
262
- name: file.name,
263
- url: file.url_private || file.permalink,
264
- size: file.size,
265
- mimetype: file.mimetype,
266
- },
267
- });
268
- }
269
- }
270
- }
271
- // Handle attachments
272
- if ("attachments" in msg && msg.attachments) {
273
- for (const attachment of msg.attachments) {
274
- if (attachment.image_url) {
275
- segments.push({
276
- type: "image",
277
- data: {
278
- url: attachment.image_url,
279
- title: attachment.title,
280
- text: attachment.text,
281
- },
282
- });
283
- }
284
- }
285
- }
286
- return segments.length > 0
287
- ? segments
288
- : [{ type: "text", data: { text: "" } }];
289
- }
290
- parseSlackText(text) {
291
- const segments = [];
292
- let lastIndex = 0;
293
- // Match user mentions <@U12345678>
294
- const userMentionRegex = /<@([UW][A-Z0-9]+)(?:\|([^>]+))?>/g;
295
- // Match channel mentions <#C12345678|general>
296
- const channelMentionRegex = /<#([C][A-Z0-9]+)(?:\|([^>]+))?>/g;
297
- // Match links <http://example.com|Example>
298
- const linkRegex = /<(https?:\/\/[^|>]+)(?:\|([^>]+))?>/g;
299
- const allMatches = [];
300
- // Collect all matches
301
- let match;
302
- while ((match = userMentionRegex.exec(text)) !== null) {
303
- allMatches.push({ match, type: "user" });
304
- }
305
- while ((match = channelMentionRegex.exec(text)) !== null) {
306
- allMatches.push({ match, type: "channel" });
307
- }
308
- while ((match = linkRegex.exec(text)) !== null) {
309
- allMatches.push({ match, type: "link" });
310
- }
311
- // Sort by position
312
- allMatches.sort((a, b) => a.match.index - b.match.index);
313
- // Process matches
314
- for (const { match, type } of allMatches) {
315
- const matchStart = match.index;
316
- const matchEnd = matchStart + match[0].length;
317
- // Add text before match
318
- if (matchStart > lastIndex) {
319
- const beforeText = text.slice(lastIndex, matchStart);
320
- if (beforeText.trim()) {
321
- segments.push({ type: "text", data: { text: beforeText } });
322
- }
323
- }
324
- // Add special segment
325
- switch (type) {
326
- case "user":
327
- segments.push({
328
- type: "at",
329
- data: {
330
- id: match[1],
331
- name: match[2] || match[1],
332
- text: match[0],
333
- },
334
- });
335
- break;
336
- case "channel":
337
- segments.push({
338
- type: "channel_mention",
339
- data: {
340
- id: match[1],
341
- name: match[2] || match[1],
342
- text: match[0],
343
- },
344
- });
345
- break;
346
- case "link":
347
- segments.push({
348
- type: "link",
349
- data: {
350
- url: match[1],
351
- text: match[2] || match[1],
352
- },
353
- });
354
- break;
355
- }
356
- lastIndex = matchEnd;
357
- }
358
- // Add remaining text
359
- if (lastIndex < text.length) {
360
- const remainingText = text.slice(lastIndex);
361
- if (remainingText.trim()) {
362
- segments.push({ type: "text", data: { text: remainingText } });
363
- }
364
- }
365
- return segments.length > 0
366
- ? segments
367
- : [{ type: "text", data: { text } }];
368
147
  }
369
- async $sendMessage(options) {
370
- try {
371
- const canonical = expandInteractiveSegmentsInContent(options.content);
372
- const wire = fromCanonicalSegments(canonical);
373
- const result = await this.sendContentToChannel(options.id, wire);
374
- this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`);
375
- return result.ts || "";
376
- }
377
- catch (error) {
378
- this.logger.error("Failed to send Slack message:", error);
379
- throw error;
380
- }
148
+ admitInteraction(payload) {
149
+ if (!this.#open)
150
+ return;
151
+ if (payload.type !== 'block_actions' || !payload.actions?.length)
152
+ return;
153
+ const channelId = payload.channel?.id ?? '';
154
+ const userId = payload.user.id;
155
+ const messageTs = payload.message?.ts ?? '';
156
+ if (payload.response_url) {
157
+ postSlackEphemeral(payload.response_url, '已收到', logger);
158
+ }
159
+ void this.#options.gateway.receive({
160
+ adapter: this.#options.id,
161
+ target: channelId || userId,
162
+ content: formatInteractionContent(payload),
163
+ sender: userId,
164
+ id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
165
+ metadata: Object.freeze({
166
+ endpoint: this.#options.config.name,
167
+ eventType: 'block_actions',
168
+ actionId: payload.actions[0]?.action_id,
169
+ threadTs: messageTs || undefined,
170
+ }),
171
+ }).catch((err) => {
172
+ logger.warn(formatCompact({
173
+ op: 'slack_gateway_receive_failed',
174
+ target: channelId,
175
+ error: err instanceof Error ? err.message : String(err),
176
+ }));
177
+ });
381
178
  }
382
- async sendContentToChannel(channel, content, extraOptions = {}) {
383
- if (!Array.isArray(content))
384
- content = [content];
385
- let textContent = "";
386
- const attachments = [];
387
- for (const segment of content) {
388
- if (typeof segment === "string") {
389
- textContent += segment;
390
- continue;
391
- }
392
- const { type, data } = segment;
393
- switch (type) {
394
- case "text":
395
- textContent += data.text || "";
396
- break;
397
- case "at":
398
- textContent += `<@${data.id}>`;
399
- break;
400
- case "channel_mention":
401
- textContent += `<#${data.id}>`;
402
- break;
403
- case "link":
404
- if (data.text && data.text !== data.url) {
405
- textContent += `<${data.url}|${data.text}>`;
406
- }
407
- else {
408
- textContent += `<${data.url}>`;
409
- }
410
- break;
411
- case "image":
412
- if (data.url) {
413
- attachments.push({
414
- image_url: data.url,
415
- title: data.name || data.title,
416
- });
417
- }
418
- break;
419
- case "file":
420
- // Files need to be uploaded separately
421
- if (data.file) {
422
- try {
423
- await this.client.files.upload({
424
- channels: channel,
425
- file: data.file,
426
- filename: data.name,
427
- });
428
- }
429
- catch (error) {
430
- this.logger.error("Failed to upload file:", error);
431
- }
432
- }
433
- break;
434
- default:
435
- textContent += data.text || `[${type}]`;
436
- }
437
- }
438
- // Send message
439
- const messageOptions = {
440
- channel,
441
- text: textContent.trim() || "Message",
442
- ...extraOptions,
443
- };
444
- if (attachments.length > 0) {
445
- messageOptions.attachments = attachments;
179
+ admitSlashCommand(cmd) {
180
+ if (!this.#open)
181
+ return;
182
+ postSlackEphemeral(cmd.response_url, '处理中…', logger);
183
+ void this.#options.gateway.receive({
184
+ adapter: this.#options.id,
185
+ target: cmd.channel_id,
186
+ content: formatSlashContent(cmd),
187
+ sender: cmd.user_id,
188
+ id: cmd.trigger_id,
189
+ metadata: Object.freeze({
190
+ endpoint: this.#options.config.name,
191
+ eventType: 'slash_command',
192
+ command: cmd.command,
193
+ }),
194
+ }).catch((err) => {
195
+ logger.warn(formatCompact({
196
+ op: 'slack_gateway_receive_failed',
197
+ target: cmd.channel_id,
198
+ error: err instanceof Error ? err.message : String(err),
199
+ }));
200
+ });
201
+ }
202
+ handleEnvelope(body) {
203
+ const envelope = body;
204
+ if (envelope?.type === 'event_callback' && envelope.event) {
205
+ this.admit(envelope.event);
446
206
  }
447
- const result = await this.client.chat.postMessage(messageOptions);
448
- return result.message || {};
449
- }
450
- async $recallMessage(id) {
451
- // Slack requires both channel and ts (timestamp) to delete a message
452
- // The Endpoint interface only provides message ID (ts), making recall impossible
453
- // Users should use message.$recall() instead, which has the full context
454
- throw new Error("SlackEndpoint.$recallMessage: Message recall not supported without channel information. " +
455
- "Use message.$recall() method instead, which contains the required context.");
456
- }
457
- // ==================== 工作区管理 API ====================
458
- /**
459
- * 邀请用户到频道
460
- * @param channel 频道 ID
461
- * @param users 用户 ID 列表
462
- */
207
+ }
208
+ trackMessageChannel(ts, channel) {
209
+ if (ts && channel)
210
+ this.#messageChannelMap.set(ts, channel);
211
+ }
212
+ resolveMessageRef(messageId, channelHint) {
213
+ const parsed = parseSlackMessageRef(messageId);
214
+ if (parsed) {
215
+ this.trackMessageChannel(parsed.ts, parsed.channel);
216
+ return parsed;
217
+ }
218
+ if (channelHint)
219
+ return { channel: channelHint, ts: messageId };
220
+ const channel = this.#messageChannelMap.get(messageId);
221
+ return channel ? { channel, ts: messageId } : null;
222
+ }
223
+ async editMessage(channel, messageTs, content) {
224
+ if (!this.#client)
225
+ throw new Error('Slack client not connected');
226
+ await editSlackContent(this.#client, channel, messageTs, content);
227
+ }
228
+ // ── Agent tool surface ──────────────────────────────────────────────
463
229
  async inviteToChannel(channel, users) {
464
- try {
465
- await this.client.conversations.invite({ channel, users: users.join(',') });
466
- this.logger.debug(formatCompact({ op: "invite", endpoint: this.$id, channel, users: users.join(",") }));
467
- return true;
468
- }
469
- catch (error) {
470
- this.logger.error(`Slack Endpoint ${this.$id} 邀请用户失败:`, error);
471
- throw error;
472
- }
230
+ await this.#client.conversations.invite({ channel, users: users.join(',') });
231
+ return true;
473
232
  }
474
- /**
475
- * 从频道踢出用户
476
- * @param channel 频道 ID
477
- * @param user 用户 ID
478
- */
479
233
  async kickFromChannel(channel, user) {
480
- try {
481
- await this.client.conversations.kick({ channel, user });
482
- this.logger.debug(formatCompact({ op: "kick", endpoint: this.$id, channel, user }));
483
- return true;
484
- }
485
- catch (error) {
486
- this.logger.error(`Slack Endpoint ${this.$id} 踢出用户失败:`, error);
487
- throw error;
488
- }
234
+ await this.#client.conversations.kick({ channel, user });
235
+ return true;
489
236
  }
490
- /**
491
- * 设置频道话题
492
- * @param channel 频道 ID
493
- * @param topic 话题
494
- */
495
237
  async setChannelTopic(channel, topic) {
496
- try {
497
- await this.client.conversations.setTopic({ channel, topic });
498
- this.logger.debug(formatCompact({ op: "set_topic", endpoint: this.$id, channel }));
499
- return true;
500
- }
501
- catch (error) {
502
- this.logger.error(`Slack Endpoint ${this.$id} 设置话题失败:`, error);
503
- throw error;
504
- }
238
+ await this.#client.conversations.setTopic({ channel, topic });
239
+ return true;
505
240
  }
506
- /**
507
- * 设置频道目的
508
- * @param channel 频道 ID
509
- * @param purpose 目的
510
- */
511
241
  async setChannelPurpose(channel, purpose) {
512
- try {
513
- await this.client.conversations.setPurpose({ channel, purpose });
514
- this.logger.debug(formatCompact({ op: "set_purpose", endpoint: this.$id, channel }));
515
- return true;
516
- }
517
- catch (error) {
518
- this.logger.error(`Slack Endpoint ${this.$id} 设置目的失败:`, error);
519
- throw error;
520
- }
242
+ await this.#client.conversations.setPurpose({ channel, purpose });
243
+ return true;
521
244
  }
522
- /**
523
- * 归档频道
524
- * @param channel 频道 ID
525
- */
526
245
  async archiveChannel(channel) {
527
- try {
528
- await this.client.conversations.archive({ channel });
529
- this.logger.debug(formatCompact({ op: "archive", endpoint: this.$id, channel }));
530
- return true;
531
- }
532
- catch (error) {
533
- this.logger.error(`Slack Endpoint ${this.$id} 归档频道失败:`, error);
534
- throw error;
535
- }
246
+ await this.#client.conversations.archive({ channel });
247
+ return true;
536
248
  }
537
- /**
538
- * 取消归档频道
539
- * @param channel 频道 ID
540
- */
541
249
  async unarchiveChannel(channel) {
542
- try {
543
- await this.client.conversations.unarchive({ channel });
544
- this.logger.debug(formatCompact({ op: "unarchive", endpoint: this.$id, channel }));
545
- return true;
546
- }
547
- catch (error) {
548
- this.logger.error(`Slack Endpoint ${this.$id} 取消归档失败:`, error);
549
- throw error;
550
- }
250
+ await this.#client.conversations.unarchive({ channel });
251
+ return true;
551
252
  }
552
- /**
553
- * 重命名频道
554
- * @param channel 频道 ID
555
- * @param name 新名称
556
- */
557
253
  async renameChannel(channel, name) {
558
- try {
559
- await this.client.conversations.rename({ channel, name });
560
- this.logger.debug(formatCompact({ op: "rename", endpoint: this.$id, channel, name }));
561
- return true;
562
- }
563
- catch (error) {
564
- this.logger.error(`Slack Endpoint ${this.$id} 重命名频道失败:`, error);
565
- throw error;
566
- }
254
+ await this.#client.conversations.rename({ channel, name });
255
+ return true;
567
256
  }
568
- /**
569
- * 获取频道成员列表
570
- * @param channel 频道 ID
571
- */
572
257
  async getChannelMembers(channel) {
573
- try {
574
- const result = await this.client.conversations.members({ channel });
575
- return result.members || [];
576
- }
577
- catch (error) {
578
- this.logger.error(`Slack Endpoint ${this.$id} 获取成员列表失败:`, error);
579
- throw error;
580
- }
258
+ const result = await this.#client.conversations.members({ channel });
259
+ return result.members || [];
581
260
  }
582
- /**
583
- * 获取频道信息
584
- * @param channel 频道 ID
585
- */
586
261
  async getChannelInfo(channel) {
587
- try {
588
- const result = await this.client.conversations.info({ channel });
589
- return result.channel;
590
- }
591
- catch (error) {
592
- this.logger.error(`Slack Endpoint ${this.$id} 获取频道信息失败:`, error);
593
- throw error;
594
- }
262
+ const result = await this.#client.conversations.info({ channel });
263
+ return result.channel;
595
264
  }
596
- /**
597
- * 获取用户信息
598
- * @param user 用户 ID
599
- */
600
265
  async getUserInfo(user) {
601
- try {
602
- const result = await this.client.users.info({ user });
603
- return result.user;
604
- }
605
- catch (error) {
606
- this.logger.error(`Slack Endpoint ${this.$id} 获取用户信息失败:`, error);
607
- throw error;
608
- }
266
+ const result = await this.#client.users.info({ user });
267
+ return result.user;
609
268
  }
610
- /**
611
- * 添加消息反应
612
- * @param channel 频道 ID
613
- * @param timestamp 消息时间戳
614
- * @param name 表情名称
615
- */
616
269
  async addReaction(channel, timestamp, name) {
270
+ const reaction = normalizeSlackReactionName(name);
617
271
  try {
618
- await this.client.reactions.add({ channel, timestamp, name });
619
- this.logger.debug(formatCompact({ op: "reaction_add", endpoint: this.$id, name }));
272
+ await this.#client.reactions.add({ channel, timestamp, name: reaction });
620
273
  return true;
621
274
  }
622
275
  catch (error) {
623
- this.logger.error(`Slack Endpoint ${this.$id} 添加反应失败:`, error);
276
+ const code = error?.data?.error;
277
+ if (code === 'already_reacted')
278
+ return true;
624
279
  throw error;
625
280
  }
626
281
  }
627
- /**
628
- * 移除消息反应
629
- * @param channel 频道 ID
630
- * @param timestamp 消息时间戳
631
- * @param name 表情名称
632
- */
633
282
  async removeReaction(channel, timestamp, name) {
283
+ const reaction = normalizeSlackReactionName(name);
634
284
  try {
635
- await this.client.reactions.remove({ channel, timestamp, name });
636
- this.logger.debug(formatCompact({ op: "reaction_remove", endpoint: this.$id, name }));
285
+ await this.#client.reactions.remove({ channel, timestamp, name: reaction });
637
286
  return true;
638
287
  }
639
288
  catch (error) {
640
- this.logger.error(`Slack Endpoint ${this.$id} 移除反应失败:`, error);
289
+ const code = error?.data?.error;
290
+ if (code === 'no_reaction')
291
+ return true;
641
292
  throw error;
642
293
  }
643
294
  }
644
- /**
645
- * 置顶消息
646
- * @param channel 频道 ID
647
- * @param timestamp 消息时间戳
648
- */
649
295
  async pinMessage(channel, timestamp) {
650
- try {
651
- await this.client.pins.add({ channel, timestamp });
652
- this.logger.debug(formatCompact({ op: "pin", endpoint: this.$id, channel }));
653
- return true;
654
- }
655
- catch (error) {
656
- this.logger.error(`Slack Endpoint ${this.$id} 置顶消息失败:`, error);
657
- throw error;
658
- }
296
+ await this.#client.pins.add({ channel, timestamp });
297
+ return true;
659
298
  }
660
- /**
661
- * 取消置顶消息
662
- * @param channel 频道 ID
663
- * @param timestamp 消息时间戳
664
- */
665
299
  async unpinMessage(channel, timestamp) {
666
- try {
667
- await this.client.pins.remove({ channel, timestamp });
668
- this.logger.debug(formatCompact({ op: "unpin", endpoint: this.$id, channel }));
669
- return true;
670
- }
671
- catch (error) {
672
- this.logger.error(`Slack Endpoint ${this.$id} 取消置顶失败:`, error);
673
- throw error;
674
- }
300
+ await this.#client.pins.remove({ channel, timestamp });
301
+ return true;
302
+ }
303
+ async #startSocket() {
304
+ const { config } = this.#options;
305
+ if (!config.appToken)
306
+ throw new Error('Socket Mode requires appToken');
307
+ this.#socket = this.#options.createSocket?.({
308
+ appToken: config.appToken,
309
+ clientPingTimeout: config.clientPingTimeout,
310
+ }) ?? new SocketModeClient({
311
+ appToken: config.appToken,
312
+ clientPingTimeout: config.clientPingTimeout,
313
+ });
314
+ this.#socket.on('slack_event', async ({ ack, body }) => {
315
+ await ack();
316
+ this.handleEnvelope(body);
317
+ });
318
+ this.#socket.on('interactive', async ({ ack, body }) => {
319
+ await ack();
320
+ this.admitInteraction(body);
321
+ });
322
+ this.#socket.on('slash_commands', async ({ ack, body }) => {
323
+ await ack();
324
+ this.admitSlashCommand(body);
325
+ });
326
+ await this.#socket.start();
327
+ }
328
+ }
329
+ function parseSendTarget(target) {
330
+ const parsed = parseSlackMessageRef(target);
331
+ if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
332
+ return { channel: parsed.channel, threadTs: parsed.ts };
675
333
  }
334
+ return { channel: target };
676
335
  }
677
- //# sourceMappingURL=endpoint.js.map