@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.
- package/CHANGELOG.md +59 -0
- package/README.md +77 -160
- package/adapters/slack.ts +34 -0
- package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
- package/agent/tools/add_reaction.ts +26 -0
- package/agent/tools/archive_channel.ts +24 -0
- package/agent/tools/edit_message.ts +28 -0
- package/agent/tools/invite_to_channel.ts +26 -0
- package/agent/tools/pin_message.ts +26 -0
- package/agent/tools/remove_reaction.ts +26 -0
- package/agent/tools/set_purpose.ts +26 -0
- package/agent/tools/set_topic.ts +26 -0
- package/agent/tools/unarchive.ts +24 -0
- package/agent/tools/unpin_message.ts +26 -0
- package/agent/tools/user_info.ts +31 -0
- package/lib/endpoint.d.ts +133 -94
- package/lib/endpoint.js +270 -612
- package/lib/index.d.ts +11 -10
- package/lib/index.js +11 -252
- package/lib/markdown-to-mrkdwn.d.ts +9 -0
- package/lib/markdown-to-mrkdwn.js +121 -0
- package/lib/mrkdwn-to-markdown.d.ts +4 -0
- package/lib/mrkdwn-to-markdown.js +30 -0
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +143 -0
- package/lib/protocol.js +225 -0
- package/lib/slack-agent-deps.d.ts +28 -0
- package/lib/slack-agent-deps.js +30 -0
- package/lib/slack-inbound-filter.d.ts +12 -0
- package/lib/slack-inbound-filter.js +46 -0
- package/lib/slack-message-ref.d.ts +7 -0
- package/lib/slack-message-ref.js +17 -0
- package/lib/slack-outbound.d.ts +24 -0
- package/lib/slack-outbound.js +109 -0
- package/lib/slack-reaction.d.ts +1 -0
- package/lib/slack-reaction.js +26 -0
- package/lib/slack-response-url.d.ts +5 -0
- package/lib/slack-response-url.js +16 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +69 -0
- package/package.json +53 -14
- package/plugin.ts +13 -0
- package/schema.json +38 -0
- package/src/endpoint.ts +339 -647
- package/src/index.ts +65 -277
- package/src/markdown-to-mrkdwn.ts +117 -0
- package/src/mrkdwn-to-markdown.ts +29 -0
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +380 -0
- package/src/slack-agent-deps.ts +57 -0
- package/src/slack-inbound-filter.ts +60 -0
- package/src/slack-message-ref.ts +18 -0
- package/src/slack-outbound.ts +167 -0
- package/src/slack-reaction.ts +23 -0
- package/src/slack-response-url.ts +26 -0
- package/src/webhook.ts +103 -0
- package/lib/adapter.d.ts +0 -19
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -46
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -17
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -54
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -18
- /package/{skills/slack → agent}/PERMITS.md +0 -0
package/lib/endpoint.js
CHANGED
|
@@ -1,677 +1,335 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { WebClient } from
|
|
6
|
-
import { formatCompact,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
51
|
-
this.#
|
|
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
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
79
|
-
|
|
74
|
+
await this.stop();
|
|
75
|
+
logger.error('Failed to connect Slack endpoint:', error);
|
|
80
76
|
throw error;
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
117
|
-
if (channel?.creator === userId)
|
|
118
|
-
isChannelManager = true;
|
|
89
|
+
await this.#socket.disconnect();
|
|
119
90
|
}
|
|
120
91
|
catch {
|
|
121
|
-
|
|
92
|
+
/* ignore */
|
|
122
93
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
if (!
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
559
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|