@zhin.js/adapter-slack 1.0.78 → 1.1.2
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 +731 -2
- package/PERMITS.md +24 -0
- package/README.md +111 -158
- package/adapters/slack/index.js +40 -0
- package/adapters/slack/index.ts +45 -0
- package/agents/slack/agent.json +20 -0
- package/agents/slack/boundaries.md +3 -0
- package/agents/slack/conventions.md +3 -0
- package/agents/slack/skills/slack-channels/SKILL.md +24 -0
- package/agents/slack/skills/slack-channels/tools/archive_channel/index.js +19 -0
- package/agents/slack/skills/slack-channels/tools/archive_channel/index.ts +21 -0
- package/agents/slack/skills/slack-channels/tools/invite_to_channel/index.js +20 -0
- package/agents/slack/skills/slack-channels/tools/invite_to_channel/index.ts +23 -0
- package/agents/slack/skills/slack-channels/tools/set_purpose/index.js +20 -0
- package/agents/slack/skills/slack-channels/tools/set_purpose/index.ts +23 -0
- package/agents/slack/skills/slack-channels/tools/set_topic/index.js +20 -0
- package/agents/slack/skills/slack-channels/tools/set_topic/index.ts +23 -0
- package/agents/slack/skills/slack-channels/tools/unarchive/index.js +19 -0
- package/agents/slack/skills/slack-channels/tools/unarchive/index.ts +21 -0
- package/agents/slack/skills/slack-channels/tools/user_info/index.js +27 -0
- package/agents/slack/skills/slack-channels/tools/user_info/index.ts +29 -0
- package/agents/slack/skills/slack-messages/SKILL.md +22 -0
- package/agents/slack/skills/slack-messages/tools/add_reaction/index.js +27 -0
- package/agents/slack/skills/slack-messages/tools/add_reaction/index.ts +29 -0
- package/agents/slack/skills/slack-messages/tools/edit_message/index.js +21 -0
- package/agents/slack/skills/slack-messages/tools/edit_message/index.ts +25 -0
- package/agents/slack/skills/slack-messages/tools/pin_message/index.js +20 -0
- package/agents/slack/skills/slack-messages/tools/pin_message/index.ts +23 -0
- package/agents/slack/skills/slack-messages/tools/remove_reaction/index.js +30 -0
- package/agents/slack/skills/slack-messages/tools/remove_reaction/index.ts +32 -0
- package/agents/slack/skills/slack-messages/tools/unpin_message/index.js +20 -0
- package/agents/slack/skills/slack-messages/tools/unpin_message/index.ts +23 -0
- package/agents/slack/system.md +3 -0
- package/commands/slack/endpoint/add/[id]/index.js +3 -0
- package/commands/slack/endpoint/add/[id]/index.ts +3 -0
- package/commands/slack/endpoint/definition.js +19 -0
- package/commands/slack/endpoint/definition.ts +19 -0
- package/commands/slack/endpoint/list/index.js +3 -0
- package/commands/slack/endpoint/list/index.ts +3 -0
- package/commands/slack/endpoint/remove/[id]/index.js +3 -0
- package/commands/slack/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +25 -0
- package/lib/client.js +2 -0
- package/lib/endpoint.d.ts +153 -0
- package/lib/endpoint.js +425 -0
- package/lib/index.d.ts +10 -10
- package/lib/index.js +10 -242
- 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 +15 -0
- package/lib/platform-permit.js +43 -0
- package/lib/protocol.d.ts +144 -0
- package/lib/protocol.js +258 -0
- package/lib/side-event-dispatch.d.ts +4 -0
- package/lib/side-event-dispatch.js +45 -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 +111 -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/slack-runtime-state.d.ts +1 -0
- package/lib/slack-runtime-state.js +6 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +72 -0
- package/package.json +80 -16
- package/plugin.js +19 -0
- package/schema.json +108 -0
- package/src/client.ts +30 -0
- package/src/endpoint.ts +526 -0
- package/src/index.ts +59 -265
- package/src/markdown-to-mrkdwn.ts +117 -0
- package/src/mrkdwn-to-markdown.ts +29 -0
- package/src/platform-permit.ts +59 -0
- package/src/protocol.ts +406 -0
- package/src/side-event-dispatch.ts +54 -0
- package/src/slack-inbound-filter.ts +60 -0
- package/src/slack-message-ref.ts +18 -0
- package/src/slack-outbound.ts +170 -0
- package/src/slack-reaction.ts +23 -0
- package/src/slack-response-url.ts +26 -0
- package/src/slack-runtime-state.ts +7 -0
- package/src/webhook.ts +106 -0
- package/lib/adapter.d.ts +0 -16
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -43
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -106
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -628
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.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/skills/slack/SKILL.md +0 -91
- package/src/adapter.ts +0 -52
- package/src/bot.ts +0 -688
- package/src/types.ts +0 -18
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
4
|
+
*/
|
|
5
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
6
|
+
import { WebClient } from '@slack/web-api';
|
|
7
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
|
+
import { formatInboundContent, formatInteractionContent, formatSlashContent, resolveSlackChannelType, slackInboundConversation, } from './protocol.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
|
+
import { receiveSlackSideEvent } from './side-event-dispatch.js';
|
|
16
|
+
export class SlackEndpoint extends Endpoint {
|
|
17
|
+
#logger;
|
|
18
|
+
#options;
|
|
19
|
+
#inboundFilter = createSlackInboundFilterState();
|
|
20
|
+
#messageChannelMap = new Map();
|
|
21
|
+
#client;
|
|
22
|
+
#socket;
|
|
23
|
+
#routeReleases = [];
|
|
24
|
+
#botUserId;
|
|
25
|
+
#open = false;
|
|
26
|
+
#started = false;
|
|
27
|
+
management = createSlackEndpointManagement(() => this.client);
|
|
28
|
+
control = Object.freeze({
|
|
29
|
+
recall: async (message) => {
|
|
30
|
+
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
31
|
+
if (!ref || !this.#client)
|
|
32
|
+
return;
|
|
33
|
+
await this.#client.chat.delete(ref);
|
|
34
|
+
},
|
|
35
|
+
edit: async (message, content) => {
|
|
36
|
+
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
37
|
+
if (!ref)
|
|
38
|
+
return null;
|
|
39
|
+
await this.#editMessage(ref.channel, ref.ts, content);
|
|
40
|
+
return message.id;
|
|
41
|
+
},
|
|
42
|
+
addReaction: async (message, emoji) => {
|
|
43
|
+
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
44
|
+
if (!ref)
|
|
45
|
+
return null;
|
|
46
|
+
const reaction = normalizeSlackReactionName(emoji);
|
|
47
|
+
await this.#addReaction(ref.channel, ref.ts, reaction);
|
|
48
|
+
return reaction;
|
|
49
|
+
},
|
|
50
|
+
removeReaction: async (message, reactionId) => {
|
|
51
|
+
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
52
|
+
if (!ref)
|
|
53
|
+
return;
|
|
54
|
+
await this.#removeReaction(ref.channel, ref.ts, reactionId);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super();
|
|
59
|
+
this.#logger = getAdapterLogger('slack', options.config.id);
|
|
60
|
+
this.#options = options;
|
|
61
|
+
}
|
|
62
|
+
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
63
|
+
get name() {
|
|
64
|
+
return this.#options.config.id;
|
|
65
|
+
}
|
|
66
|
+
get client() {
|
|
67
|
+
if (!this.#client)
|
|
68
|
+
throw new Error('Slack client not connected');
|
|
69
|
+
return this.#client;
|
|
70
|
+
}
|
|
71
|
+
get platformUserId() {
|
|
72
|
+
return this.#botUserId;
|
|
73
|
+
}
|
|
74
|
+
get config() {
|
|
75
|
+
return this.#options.config;
|
|
76
|
+
}
|
|
77
|
+
async start() {
|
|
78
|
+
if (this.#started)
|
|
79
|
+
return;
|
|
80
|
+
this.#started = true;
|
|
81
|
+
try {
|
|
82
|
+
const { config } = this.#options;
|
|
83
|
+
this.#client = this.#options.createClient?.(config.token)
|
|
84
|
+
?? new WebClient(config.token);
|
|
85
|
+
if (config.mode === 'socket') {
|
|
86
|
+
await this.#startSocket();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const http = this.#options.http;
|
|
90
|
+
if (!http) {
|
|
91
|
+
throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
|
|
92
|
+
}
|
|
93
|
+
this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
|
|
94
|
+
this.#logger.debug(formatCompact({
|
|
95
|
+
endpoint: config.id,
|
|
96
|
+
op: 'webhook',
|
|
97
|
+
path: config.webhookPath,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
const authTest = await this.#client.auth.test();
|
|
101
|
+
if (authTest.user_id)
|
|
102
|
+
this.#botUserId = String(authTest.user_id);
|
|
103
|
+
this.#logger.info(`connected (${config.mode})`
|
|
104
|
+
+ (this.#botUserId ? ` | user: ${this.#botUserId}` : ''));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await this.stop();
|
|
108
|
+
this.#logger.error('Failed to connect Slack endpoint:', error);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
open() {
|
|
113
|
+
this.#open = true;
|
|
114
|
+
}
|
|
115
|
+
close() {
|
|
116
|
+
this.#open = false;
|
|
117
|
+
}
|
|
118
|
+
async stop() {
|
|
119
|
+
this.#open = false;
|
|
120
|
+
if (this.#socket) {
|
|
121
|
+
try {
|
|
122
|
+
await this.#socket.disconnect();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* ignore */
|
|
126
|
+
}
|
|
127
|
+
this.#socket = undefined;
|
|
128
|
+
}
|
|
129
|
+
for (const release of this.#routeReleases.splice(0))
|
|
130
|
+
release();
|
|
131
|
+
this.#client = undefined;
|
|
132
|
+
this.#started = false;
|
|
133
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
134
|
+
}
|
|
135
|
+
async send({ conversation, payload }) {
|
|
136
|
+
if (!this.#client)
|
|
137
|
+
throw new Error('Slack client not connected');
|
|
138
|
+
const channel = conversation.id;
|
|
139
|
+
const threadTs = conversation.threadId;
|
|
140
|
+
const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, this.#logger);
|
|
141
|
+
this.trackMessageChannel(result.ts, channel);
|
|
142
|
+
return formatSlackMessageRef(channel, result.ts);
|
|
143
|
+
}
|
|
144
|
+
/** Test / internal: admit a message event when open. */
|
|
145
|
+
admit(event) {
|
|
146
|
+
if (!this.#open)
|
|
147
|
+
return;
|
|
148
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
149
|
+
if (event.type !== 'message' && event.type !== 'app_mention')
|
|
150
|
+
return;
|
|
151
|
+
const msg = event;
|
|
152
|
+
if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
|
|
153
|
+
return;
|
|
154
|
+
if (!msg.channel || !msg.ts)
|
|
155
|
+
return;
|
|
156
|
+
this.trackMessageChannel(msg.ts, msg.channel);
|
|
157
|
+
const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
|
|
158
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
159
|
+
channelId: msg.channel,
|
|
160
|
+
channelType: msg.channel_type,
|
|
161
|
+
threadId: threadTs,
|
|
162
|
+
});
|
|
163
|
+
void this.emit('message.receive', {
|
|
164
|
+
conversation,
|
|
165
|
+
message: { conversation, id: msg.ts },
|
|
166
|
+
content: formatInboundContent(msg),
|
|
167
|
+
sender: { id: msg.user ?? msg.channel ?? '' },
|
|
168
|
+
endpointId: this.#options.config.id,
|
|
169
|
+
...(msg.type === 'app_mention' ? { mentioned: true } : {}),
|
|
170
|
+
metadata: Object.freeze({
|
|
171
|
+
channelType: resolveSlackChannelType(msg),
|
|
172
|
+
userId: msg.user,
|
|
173
|
+
threadTs,
|
|
174
|
+
ts: msg.ts,
|
|
175
|
+
}),
|
|
176
|
+
}).catch((err) => {
|
|
177
|
+
this.#logger.warn(formatCompact({
|
|
178
|
+
op: 'slack_gateway_receive_failed',
|
|
179
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
180
|
+
error: err instanceof Error ? err.message : String(err),
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
admitInteraction(payload) {
|
|
185
|
+
if (!this.#open)
|
|
186
|
+
return;
|
|
187
|
+
void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
|
|
188
|
+
if (payload.type !== 'block_actions' || !payload.actions?.length)
|
|
189
|
+
return;
|
|
190
|
+
const channelId = payload.channel?.id ?? '';
|
|
191
|
+
const userId = payload.user.id;
|
|
192
|
+
const messageTs = payload.message?.ts ?? '';
|
|
193
|
+
if (payload.response_url) {
|
|
194
|
+
postSlackEphemeral(payload.response_url, '已收到', this.#logger);
|
|
195
|
+
}
|
|
196
|
+
const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
|
|
197
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
198
|
+
channelId: channelId || userId,
|
|
199
|
+
// block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
|
|
200
|
+
channelType: channelId ? undefined : 'im',
|
|
201
|
+
});
|
|
202
|
+
void this.emit('message.receive', {
|
|
203
|
+
conversation,
|
|
204
|
+
message: { conversation, id: actionTs },
|
|
205
|
+
content: formatInteractionContent(payload),
|
|
206
|
+
sender: { id: userId },
|
|
207
|
+
endpointId: this.#options.config.id,
|
|
208
|
+
metadata: Object.freeze({
|
|
209
|
+
eventType: 'block_actions',
|
|
210
|
+
actionId: payload.actions[0]?.action_id,
|
|
211
|
+
threadTs: messageTs || undefined,
|
|
212
|
+
}),
|
|
213
|
+
}).catch((err) => {
|
|
214
|
+
this.#logger.warn(formatCompact({
|
|
215
|
+
op: 'slack_gateway_receive_failed',
|
|
216
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
217
|
+
error: err instanceof Error ? err.message : String(err),
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
admitSlashCommand(cmd) {
|
|
222
|
+
if (!this.#open)
|
|
223
|
+
return;
|
|
224
|
+
void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
|
|
225
|
+
postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
|
|
226
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
227
|
+
channelId: cmd.channel_id,
|
|
228
|
+
});
|
|
229
|
+
void this.emit('message.receive', {
|
|
230
|
+
conversation,
|
|
231
|
+
message: { conversation, id: cmd.trigger_id },
|
|
232
|
+
content: formatSlashContent(cmd),
|
|
233
|
+
sender: { id: cmd.user_id, name: cmd.user_name },
|
|
234
|
+
endpointId: this.#options.config.id,
|
|
235
|
+
metadata: Object.freeze({
|
|
236
|
+
eventType: 'slash_command',
|
|
237
|
+
command: cmd.command,
|
|
238
|
+
}),
|
|
239
|
+
}).catch((err) => {
|
|
240
|
+
this.#logger.warn(formatCompact({
|
|
241
|
+
op: 'slack_gateway_receive_failed',
|
|
242
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
243
|
+
error: err instanceof Error ? err.message : String(err),
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
handleEnvelope(body) {
|
|
248
|
+
const envelope = body;
|
|
249
|
+
if (envelope?.type === 'event_callback' && envelope.event) {
|
|
250
|
+
const event = envelope.event;
|
|
251
|
+
if (event.type !== 'message' && event.type !== 'app_mention') {
|
|
252
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
253
|
+
receiveSlackSideEvent((name, payload) => this.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.admit(event);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
trackMessageChannel(ts, channel) {
|
|
260
|
+
if (!ts || !channel)
|
|
261
|
+
return;
|
|
262
|
+
// LRU:超 1024 条淘汰最久未更新的记录,避免无界增长。
|
|
263
|
+
if (this.#messageChannelMap.has(ts))
|
|
264
|
+
this.#messageChannelMap.delete(ts);
|
|
265
|
+
this.#messageChannelMap.set(ts, channel);
|
|
266
|
+
if (this.#messageChannelMap.size > 1024) {
|
|
267
|
+
const oldest = this.#messageChannelMap.keys().next().value;
|
|
268
|
+
if (oldest != null)
|
|
269
|
+
this.#messageChannelMap.delete(oldest);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
resolveMessageRef(messageId, channelHint) {
|
|
273
|
+
const parsed = parseSlackMessageRef(messageId);
|
|
274
|
+
if (parsed) {
|
|
275
|
+
this.trackMessageChannel(parsed.ts, parsed.channel);
|
|
276
|
+
return parsed;
|
|
277
|
+
}
|
|
278
|
+
if (channelHint)
|
|
279
|
+
return { channel: channelHint, ts: messageId };
|
|
280
|
+
const channel = this.#messageChannelMap.get(messageId);
|
|
281
|
+
return channel ? { channel, ts: messageId } : null;
|
|
282
|
+
}
|
|
283
|
+
async recallMessage(messageId) {
|
|
284
|
+
if (!this.#client)
|
|
285
|
+
return;
|
|
286
|
+
const ref = this.resolveMessageRef(messageId);
|
|
287
|
+
if (!ref)
|
|
288
|
+
return;
|
|
289
|
+
await this.#client.chat.delete({ channel: ref.channel, ts: ref.ts });
|
|
290
|
+
}
|
|
291
|
+
async #editMessage(channel, messageTs, content) {
|
|
292
|
+
if (!this.#client)
|
|
293
|
+
throw new Error('Slack client not connected');
|
|
294
|
+
await editSlackContent(this.#client, channel, messageTs, content);
|
|
295
|
+
}
|
|
296
|
+
async #addReaction(channel, timestamp, name) {
|
|
297
|
+
const reaction = normalizeSlackReactionName(name);
|
|
298
|
+
try {
|
|
299
|
+
await this.#client.reactions.add({ channel, timestamp, name: reaction });
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
const code = error?.data?.error;
|
|
303
|
+
if (code === 'already_reacted')
|
|
304
|
+
return;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async #removeReaction(channel, timestamp, name) {
|
|
309
|
+
const reaction = normalizeSlackReactionName(name);
|
|
310
|
+
try {
|
|
311
|
+
await this.#client.reactions.remove({ channel, timestamp, name: reaction });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const code = error?.data?.error;
|
|
315
|
+
if (code === 'no_reaction')
|
|
316
|
+
return;
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async #emitPlatformEvent(name, event) {
|
|
321
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
322
|
+
this.#logger.warn(formatCompact({
|
|
323
|
+
op: 'slack_platform_event_failed',
|
|
324
|
+
event: name,
|
|
325
|
+
error: error instanceof Error ? error.message : String(error),
|
|
326
|
+
}));
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async #startSocket() {
|
|
330
|
+
const { config } = this.#options;
|
|
331
|
+
if (!config.appToken) {
|
|
332
|
+
throw new Error('Slack Socket Mode 需要 appToken(xapp- 前缀的 App-Level Token);注意 bot token 填 token 字段(xoxb- 前缀),两者不可混用');
|
|
333
|
+
}
|
|
334
|
+
if (!config.appToken.startsWith('xapp-')) {
|
|
335
|
+
throw new Error(`Slack appToken 格式不正确:Socket Mode 需要 xapp- 前缀的 App-Level Token(当前看起来是 ${config.appToken.slice(0, 5)}…,xoxb- 是 bot token,请填到 token 字段)`);
|
|
336
|
+
}
|
|
337
|
+
this.#socket = this.#options.createSocket?.({
|
|
338
|
+
appToken: config.appToken,
|
|
339
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
340
|
+
}) ?? new SocketModeClient({
|
|
341
|
+
appToken: config.appToken,
|
|
342
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
343
|
+
});
|
|
344
|
+
this.#socket.on('slack_event', async ({ ack, body }) => {
|
|
345
|
+
await ack();
|
|
346
|
+
this.handleEnvelope(body);
|
|
347
|
+
});
|
|
348
|
+
this.#socket.on('interactive', async ({ ack, body }) => {
|
|
349
|
+
await ack();
|
|
350
|
+
this.admitInteraction(body);
|
|
351
|
+
});
|
|
352
|
+
this.#socket.on('slash_commands', async ({ ack, body }) => {
|
|
353
|
+
await ack();
|
|
354
|
+
this.admitSlashCommand(body);
|
|
355
|
+
});
|
|
356
|
+
await this.#socket.start();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
|
|
360
|
+
const SLACK_LIST_PAGE_SIZE = 1000;
|
|
361
|
+
function createSlackEndpointManagement(requireClient) {
|
|
362
|
+
return Object.freeze({
|
|
363
|
+
// Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
|
|
364
|
+
// Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
|
|
365
|
+
async listGroups() {
|
|
366
|
+
const client = requireClient();
|
|
367
|
+
const groups = [];
|
|
368
|
+
let cursor;
|
|
369
|
+
do {
|
|
370
|
+
const page = await client.conversations.list({
|
|
371
|
+
types: 'public_channel',
|
|
372
|
+
exclude_archived: true,
|
|
373
|
+
limit: SLACK_LIST_PAGE_SIZE,
|
|
374
|
+
...(cursor ? { cursor } : {}),
|
|
375
|
+
});
|
|
376
|
+
for (const value of page.channels ?? []) {
|
|
377
|
+
const channel = asRecord(value);
|
|
378
|
+
const id = typeof channel.id === 'string' ? channel.id : '';
|
|
379
|
+
if (!id)
|
|
380
|
+
continue;
|
|
381
|
+
groups.push({ group_id: id, name: String(channel.name ?? id) });
|
|
382
|
+
}
|
|
383
|
+
cursor = page.response_metadata?.next_cursor || undefined;
|
|
384
|
+
} while (cursor);
|
|
385
|
+
return groups;
|
|
386
|
+
},
|
|
387
|
+
// Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
|
|
388
|
+
async listFriends() {
|
|
389
|
+
const client = requireClient();
|
|
390
|
+
const friends = [];
|
|
391
|
+
let cursor;
|
|
392
|
+
do {
|
|
393
|
+
const page = await client.users.list({
|
|
394
|
+
limit: SLACK_LIST_PAGE_SIZE,
|
|
395
|
+
...(cursor ? { cursor } : {}),
|
|
396
|
+
});
|
|
397
|
+
for (const value of page.members ?? []) {
|
|
398
|
+
const user = asRecord(value);
|
|
399
|
+
const id = typeof user.id === 'string' ? user.id : '';
|
|
400
|
+
if (!id || user.deleted === true)
|
|
401
|
+
continue;
|
|
402
|
+
const profile = asRecord(user.profile);
|
|
403
|
+
friends.push({
|
|
404
|
+
user_id: id,
|
|
405
|
+
nickname: String(profile.real_name ?? user.real_name ?? user.name ?? id),
|
|
406
|
+
remark: '',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
cursor = page.response_metadata?.next_cursor || undefined;
|
|
410
|
+
} while (cursor);
|
|
411
|
+
return friends;
|
|
412
|
+
},
|
|
413
|
+
// conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
|
|
414
|
+
async listGroupMembers(groupId) {
|
|
415
|
+
const client = requireClient();
|
|
416
|
+
const result = await client.conversations.members({ channel: groupId });
|
|
417
|
+
return result.members ?? [];
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function asRecord(value) {
|
|
422
|
+
return value !== null && typeof value === 'object'
|
|
423
|
+
? value
|
|
424
|
+
: {};
|
|
425
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
|
|
1
|
+
export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, slackInboundConversation, verifySlackSignature, type ResolvedSlackConfig, type SlackEndpointConfig, type SlackBlockAction, type SlackEvent, type SlackEventEnvelope, type SlackInteractionPayload, type SlackMessageEvent, type SlackSlashCommand, type SlackUrlVerification, type SlackWireSegment, } from './protocol.js';
|
|
2
|
+
export { SlackEndpoint, type SlackEndpointOptions, type SlackSocketLike, type SlackWebClientLike, } from './endpoint.js';
|
|
3
|
+
export { registerSlackWebhookRoutes, handleSlackWebhookRequest, type SlackWebhookHandler, } from './webhook.js';
|
|
4
|
+
export { slackClient, type SlackClientEventMap, type SlackUserInfo, } from './client.js';
|
|
5
|
+
export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
|
|
6
|
+
export { normalizeSlackReactionName } from './slack-reaction.js';
|
|
7
|
+
export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
|
|
8
|
+
export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
9
|
+
export { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
|
|
10
|
+
export { editSlackContent, sendSlackContent } from './slack-outbound.js';
|