@zhin.js/adapter-slack 1.1.18 → 1.1.19
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 +38 -0
- package/README.md +12 -5
- package/adapters/slack/index.js +3 -23
- package/adapters/slack/index.ts +2 -23
- package/agents/slack/skills/slack-messages/tools/edit_message/index.js +3 -3
- package/agents/slack/skills/slack-messages/tools/edit_message/index.ts +2 -2
- package/commands/slack/endpoint/definition.js +0 -2
- package/commands/slack/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +16 -0
- package/lib/adapter.js +54 -0
- package/lib/client.d.ts +3 -7
- package/lib/client.js +1 -2
- package/lib/console-spec.js +6 -0
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +12 -115
- package/lib/endpoint.js +237 -183
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +8 -2
- package/lib/slack-outbound.d.ts +16 -12
- package/lib/slack-outbound.js +28 -19
- package/lib/web-client.d.ts +1 -0
- package/lib/web-client.js +4 -1
- package/package.json +18 -18
- package/plugin.js +1 -4
- package/src/adapter.ts +38 -0
- package/src/client.ts +2 -10
- package/src/console-spec.ts +6 -0
- package/src/console.ts +2 -6
- package/src/endpoint.ts +239 -226
- package/src/index.ts +1 -1
- package/src/protocol.ts +7 -4
- package/src/slack-outbound.ts +50 -37
- package/src/web-client.ts +5 -1
- package/lib/slack-runtime-state.d.ts +0 -1
- package/lib/slack-runtime-state.js +0 -6
- package/src/slack-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import { WebClient } from '@slack/web-api';
|
|
4
5
|
import { SocketModeClient } from '@slack/socket-mode';
|
|
5
|
-
import { createSlackWebApiAgent } from './web-api-proxy.js';
|
|
6
6
|
import { createSlackStreamAgent } from './stream-proxy.js';
|
|
7
7
|
import { slackSocketConnectionError } from './socket-error.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { slackWebClientOptions, slackDeliveryError } from './web-client.js';
|
|
9
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
|
|
10
10
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
11
11
|
import { formatInboundContent, formatInteractionContent, formatSlashContent, resolveSlackChannelType, slackInboundConversation, } from './protocol.js';
|
|
12
12
|
import { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
|
|
@@ -16,81 +16,79 @@ import { postSlackEphemeral } from './slack-response-url.js';
|
|
|
16
16
|
import { editSlackContent, sendSlackContent } from './slack-outbound.js';
|
|
17
17
|
import { registerSlackWebhookRoutes } from './webhook.js';
|
|
18
18
|
import { receiveSlackSideEvent } from './side-event-dispatch.js';
|
|
19
|
-
export class SlackEndpoint extends
|
|
19
|
+
export class SlackEndpoint extends WebClient {
|
|
20
|
+
[endpointSpec];
|
|
20
21
|
#logger;
|
|
21
22
|
#options;
|
|
22
23
|
#inboundFilter = createSlackInboundFilterState();
|
|
23
24
|
#messageChannelMap = new Map();
|
|
24
|
-
#client;
|
|
25
|
-
#webApiAgent;
|
|
26
25
|
#socket;
|
|
27
26
|
#lifecycle;
|
|
28
27
|
#routeReleases = [];
|
|
29
28
|
#botUserId;
|
|
30
|
-
#
|
|
29
|
+
#executionSignal;
|
|
31
30
|
#started = false;
|
|
32
|
-
management = createSlackEndpointManagement(() => this
|
|
33
|
-
control = Object.freeze({
|
|
34
|
-
recall: async (message) => {
|
|
31
|
+
#management = createSlackEndpointManagement(() => this);
|
|
32
|
+
#control = Object.freeze({
|
|
33
|
+
recall: async ({ message, signal }) => {
|
|
35
34
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
36
|
-
if (!ref || !this.#
|
|
35
|
+
if (!ref || !this.#started)
|
|
37
36
|
return;
|
|
38
|
-
await this
|
|
37
|
+
await createEndpointOperation({ signal }).run(() => this.chat.delete(ref));
|
|
39
38
|
},
|
|
40
|
-
edit: async (message, content) => {
|
|
39
|
+
edit: async ({ message, content, signal }) => {
|
|
41
40
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
42
41
|
if (!ref)
|
|
43
42
|
return null;
|
|
44
|
-
await this.#editMessage(ref.channel, ref.ts, content);
|
|
43
|
+
await this.#editMessage({ channel: ref.channel, ts: ref.ts, content, signal });
|
|
45
44
|
return message.id;
|
|
46
45
|
},
|
|
47
|
-
addReaction: async (message, emoji) => {
|
|
46
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
48
47
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
49
48
|
if (!ref)
|
|
50
49
|
return null;
|
|
51
50
|
const reaction = normalizeSlackReactionName(emoji);
|
|
52
|
-
await this.#addReaction(ref.channel, ref.ts, reaction);
|
|
51
|
+
await this.#addReaction({ channel: ref.channel, timestamp: ref.ts, name: reaction, signal });
|
|
53
52
|
return reaction;
|
|
54
53
|
},
|
|
55
|
-
removeReaction: async (message, reactionId) => {
|
|
54
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
56
55
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
57
56
|
if (!ref)
|
|
58
57
|
return;
|
|
59
|
-
await this.#removeReaction(ref.channel, ref.ts, reactionId);
|
|
58
|
+
await this.#removeReaction({ channel: ref.channel, timestamp: ref.ts, name: reactionId, signal });
|
|
60
59
|
},
|
|
61
60
|
});
|
|
62
61
|
constructor(options) {
|
|
63
|
-
super();
|
|
62
|
+
super(options.config.token, slackWebClientOptions({ ...options.webOptions, ...(options.webApiAgent ? { agent: options.webApiAgent } : {}) }));
|
|
64
63
|
this.#logger = getAdapterLogger('slack', options.config.id);
|
|
65
64
|
this.#options = options;
|
|
66
65
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
|
|
66
|
+
const endpoint = this;
|
|
67
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
68
|
+
start: signal => this.#startFramework(signal), open: () => undefined, close: () => undefined,
|
|
69
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
70
|
+
name: options.config.id, get transportState() { return endpoint.#transportState; },
|
|
71
|
+
management: this.#management, control: this.#control,
|
|
72
|
+
});
|
|
67
73
|
}
|
|
68
74
|
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
69
75
|
get name() {
|
|
70
76
|
return this.#options.config.id;
|
|
71
77
|
}
|
|
72
|
-
get
|
|
73
|
-
if (!this.#client)
|
|
74
|
-
throw new Error('Slack client not connected');
|
|
75
|
-
return this.#client;
|
|
76
|
-
}
|
|
77
|
-
get transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
|
|
78
|
+
get #transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
|
|
78
79
|
get platformUserId() {
|
|
79
80
|
return this.#botUserId;
|
|
80
81
|
}
|
|
81
82
|
get config() {
|
|
82
83
|
return this.#options.config;
|
|
83
84
|
}
|
|
84
|
-
async
|
|
85
|
+
async #startFramework(signal) {
|
|
86
|
+
this.#executionSignal = signal;
|
|
85
87
|
if (this.#started)
|
|
86
88
|
return;
|
|
87
89
|
this.#started = true;
|
|
88
90
|
try {
|
|
89
91
|
const { config } = this.#options;
|
|
90
|
-
if (config.webApiProxy && !this.#options.createClient)
|
|
91
|
-
this.#webApiAgent = createSlackWebApiAgent(config.webApiProxy);
|
|
92
|
-
this.#client = this.#options.createClient?.(config.token)
|
|
93
|
-
?? createSlackWebClient(config.token, this.#webApiAgent ? { agent: this.#webApiAgent } : {});
|
|
94
92
|
if (config.mode === 'socket') {
|
|
95
93
|
await this.#startSocket();
|
|
96
94
|
}
|
|
@@ -106,28 +104,20 @@ export class SlackEndpoint extends Endpoint {
|
|
|
106
104
|
path: config.webhookPath,
|
|
107
105
|
}));
|
|
108
106
|
}
|
|
109
|
-
const authTest = await this
|
|
107
|
+
const authTest = await this.auth.test();
|
|
110
108
|
if (authTest.user_id)
|
|
111
109
|
this.#botUserId = String(authTest.user_id);
|
|
112
110
|
this.#logger.info(`connected (${config.mode})`
|
|
113
111
|
+ (this.#botUserId ? ` | user: ${this.#botUserId}` : ''));
|
|
114
112
|
}
|
|
115
113
|
catch (error) {
|
|
116
|
-
await this
|
|
114
|
+
await this.#stopFramework();
|
|
117
115
|
this.#logger.error('Failed to connect Slack endpoint:', error);
|
|
118
116
|
throw error;
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
|
-
|
|
122
|
-
this.#
|
|
123
|
-
}
|
|
124
|
-
close() {
|
|
125
|
-
this.#open = false;
|
|
126
|
-
}
|
|
127
|
-
async stop() {
|
|
128
|
-
this.#open = false;
|
|
129
|
-
this.#webApiAgent?.destroy();
|
|
130
|
-
this.#webApiAgent = undefined;
|
|
119
|
+
async #stopFramework() {
|
|
120
|
+
this.#options.webApiAgent?.destroy();
|
|
131
121
|
await this.#lifecycle.stop();
|
|
132
122
|
if (this.#socket) {
|
|
133
123
|
try {
|
|
@@ -140,17 +130,16 @@ export class SlackEndpoint extends Endpoint {
|
|
|
140
130
|
}
|
|
141
131
|
for (const release of this.#routeReleases.splice(0))
|
|
142
132
|
release();
|
|
143
|
-
this.#client = undefined;
|
|
144
133
|
this.#started = false;
|
|
145
134
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
146
135
|
}
|
|
147
|
-
async
|
|
148
|
-
if (!this.#
|
|
136
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
137
|
+
if (!this.#started)
|
|
149
138
|
throw new Error('Slack client not connected');
|
|
150
139
|
const channel = conversation.id;
|
|
151
140
|
const threadTs = conversation.threadId;
|
|
152
141
|
try {
|
|
153
|
-
const result = await sendSlackContent(this
|
|
142
|
+
const result = await sendSlackContent(this, { content: payload, channel, threadTs, logger: this.#logger, signal });
|
|
154
143
|
this.trackMessageChannel(result.ts, channel);
|
|
155
144
|
return formatSlackMessageRef(channel, result.ts);
|
|
156
145
|
}
|
|
@@ -159,123 +148,131 @@ export class SlackEndpoint extends Endpoint {
|
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
/** Test / internal: admit a message event when open. */
|
|
162
|
-
admit(event) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
151
|
+
admit(event, original = event) {
|
|
152
|
+
void this[endpointSpec].events.run(() => {
|
|
153
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
154
|
+
if (event.type !== 'message' && event.type !== 'app_mention')
|
|
155
|
+
return;
|
|
156
|
+
const msg = event;
|
|
157
|
+
if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
|
|
158
|
+
return;
|
|
159
|
+
if (!msg.channel || !msg.ts)
|
|
160
|
+
return;
|
|
161
|
+
this.trackMessageChannel(msg.ts, msg.channel);
|
|
162
|
+
const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
|
|
163
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
164
|
+
channelId: msg.channel,
|
|
165
|
+
channelType: msg.channel_type,
|
|
166
|
+
threadId: threadTs,
|
|
167
|
+
});
|
|
168
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
169
|
+
conversation,
|
|
170
|
+
event: original,
|
|
171
|
+
...(typeof msg.ts === 'string' && Number.isFinite(Number(msg.ts)) ? { platformSentAt: Number(msg.ts) * 1_000 } : {}),
|
|
172
|
+
message: { conversation, id: msg.ts },
|
|
173
|
+
content: formatInboundContent(msg),
|
|
174
|
+
sender: { id: msg.user ?? msg.channel ?? '' },
|
|
175
|
+
endpointId: this.#options.config.id,
|
|
176
|
+
...(msg.type === 'app_mention' ? { mentioned: true } : {}),
|
|
177
|
+
metadata: Object.freeze({
|
|
178
|
+
channelType: resolveSlackChannelType(msg),
|
|
179
|
+
userId: msg.user,
|
|
180
|
+
threadTs,
|
|
181
|
+
ts: msg.ts,
|
|
182
|
+
}),
|
|
183
|
+
}).catch((err) => {
|
|
184
|
+
this.#logger.warn(formatCompact({
|
|
185
|
+
op: 'slack_gateway_receive_failed',
|
|
186
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
187
|
+
error: err instanceof Error ? err.message : String(err),
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
200
191
|
}
|
|
201
192
|
admitInteraction(payload) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
193
|
+
void this[endpointSpec].events.run(() => {
|
|
194
|
+
void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
|
|
195
|
+
if (payload.type !== 'block_actions' || !payload.actions?.length)
|
|
196
|
+
return;
|
|
197
|
+
const channelId = payload.channel?.id ?? '';
|
|
198
|
+
const userId = payload.user.id;
|
|
199
|
+
const messageTs = payload.message?.ts ?? '';
|
|
200
|
+
const sourceThread = payload.message?.thread_ts;
|
|
201
|
+
const threadTs = typeof sourceThread === 'string' && sourceThread && sourceThread !== messageTs ? sourceThread : undefined;
|
|
202
|
+
if (payload.response_url) {
|
|
203
|
+
postSlackEphemeral(payload.response_url, '已收到', this.#logger);
|
|
204
|
+
}
|
|
205
|
+
const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
|
|
206
|
+
const platformSentAt = Number(payload.actions[0]?.action_ts) * 1_000;
|
|
207
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
208
|
+
channelId: channelId || userId,
|
|
209
|
+
// block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
|
|
210
|
+
channelType: channelId ? undefined : 'im',
|
|
211
|
+
threadId: threadTs,
|
|
212
|
+
});
|
|
213
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
214
|
+
conversation,
|
|
215
|
+
event: payload,
|
|
216
|
+
...(Number.isFinite(platformSentAt) ? { platformSentAt } : {}),
|
|
217
|
+
message: { conversation, id: actionTs },
|
|
218
|
+
content: formatInteractionContent(payload),
|
|
219
|
+
segments: [{ type: 'action', data: { id: actionTs, payload: payload.actions[0].value ?? payload.actions[0].action_id, ...(messageTs ? { sourceMessageId: messageTs } : {}) } }],
|
|
220
|
+
sender: { id: userId },
|
|
221
|
+
endpointId: this.#options.config.id,
|
|
222
|
+
metadata: Object.freeze({
|
|
223
|
+
eventType: 'block_actions',
|
|
224
|
+
actionId: payload.actions[0]?.action_id,
|
|
225
|
+
threadTs,
|
|
226
|
+
}),
|
|
227
|
+
}).catch((err) => {
|
|
228
|
+
this.#logger.warn(formatCompact({
|
|
229
|
+
op: 'slack_gateway_receive_failed',
|
|
230
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
231
|
+
error: err instanceof Error ? err.message : String(err),
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
241
235
|
}
|
|
242
236
|
admitSlashCommand(cmd) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
237
|
+
void this[endpointSpec].events.run(() => {
|
|
238
|
+
void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
|
|
239
|
+
postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
|
|
240
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
241
|
+
channelId: cmd.channel_id,
|
|
242
|
+
});
|
|
243
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
244
|
+
conversation,
|
|
245
|
+
message: { conversation, id: cmd.trigger_id },
|
|
246
|
+
event: cmd,
|
|
247
|
+
content: formatSlashContent(cmd),
|
|
248
|
+
sender: { id: cmd.user_id, name: cmd.user_name },
|
|
249
|
+
endpointId: this.#options.config.id,
|
|
250
|
+
metadata: Object.freeze({
|
|
251
|
+
eventType: 'slash_command',
|
|
252
|
+
command: cmd.command,
|
|
253
|
+
}),
|
|
254
|
+
}).catch((err) => {
|
|
255
|
+
this.#logger.warn(formatCompact({
|
|
256
|
+
op: 'slack_gateway_receive_failed',
|
|
257
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
258
|
+
error: err instanceof Error ? err.message : String(err),
|
|
259
|
+
}));
|
|
260
|
+
});
|
|
261
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
267
262
|
}
|
|
268
263
|
handleEnvelope(body) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
264
|
+
void this[endpointSpec].events.run(() => {
|
|
265
|
+
const envelope = body;
|
|
266
|
+
if (envelope?.type === 'event_callback' && envelope.event) {
|
|
267
|
+
const event = envelope.event;
|
|
268
|
+
if (event.type !== 'message' && event.type !== 'app_mention') {
|
|
269
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
270
|
+
receiveSlackSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.admit(event, body);
|
|
276
274
|
}
|
|
277
|
-
|
|
278
|
-
}
|
|
275
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
279
276
|
}
|
|
280
277
|
trackMessageChannel(ts, channel) {
|
|
281
278
|
if (!ts || !channel)
|
|
@@ -302,22 +299,22 @@ export class SlackEndpoint extends Endpoint {
|
|
|
302
299
|
return channel ? { channel, ts: messageId } : null;
|
|
303
300
|
}
|
|
304
301
|
async recallMessage(messageId) {
|
|
305
|
-
if (!this.#
|
|
302
|
+
if (!this.#started)
|
|
306
303
|
return;
|
|
307
304
|
const ref = this.resolveMessageRef(messageId);
|
|
308
305
|
if (!ref)
|
|
309
306
|
return;
|
|
310
|
-
await this
|
|
307
|
+
await this.chat.delete({ channel: ref.channel, ts: ref.ts });
|
|
311
308
|
}
|
|
312
|
-
async #editMessage(
|
|
313
|
-
if (!this.#
|
|
309
|
+
async #editMessage(request) {
|
|
310
|
+
if (!this.#started)
|
|
314
311
|
throw new Error('Slack client not connected');
|
|
315
|
-
await editSlackContent(this
|
|
312
|
+
await editSlackContent(this, request);
|
|
316
313
|
}
|
|
317
|
-
async #addReaction(channel, timestamp, name) {
|
|
314
|
+
async #addReaction({ channel, timestamp, name, signal }) {
|
|
318
315
|
const reaction = normalizeSlackReactionName(name);
|
|
319
316
|
try {
|
|
320
|
-
await this
|
|
317
|
+
await createEndpointOperation({ signal }).run(() => this.reactions.add({ channel, timestamp, name: reaction }));
|
|
321
318
|
}
|
|
322
319
|
catch (error) {
|
|
323
320
|
const code = error?.data?.error;
|
|
@@ -326,10 +323,10 @@ export class SlackEndpoint extends Endpoint {
|
|
|
326
323
|
throw error;
|
|
327
324
|
}
|
|
328
325
|
}
|
|
329
|
-
async #removeReaction(channel, timestamp, name) {
|
|
326
|
+
async #removeReaction({ channel, timestamp, name, signal }) {
|
|
330
327
|
const reaction = normalizeSlackReactionName(name);
|
|
331
328
|
try {
|
|
332
|
-
await this
|
|
329
|
+
await createEndpointOperation({ signal }).run(() => this.reactions.remove({ channel, timestamp, name: reaction }));
|
|
333
330
|
}
|
|
334
331
|
catch (error) {
|
|
335
332
|
const code = error?.data?.error;
|
|
@@ -339,7 +336,7 @@ export class SlackEndpoint extends Endpoint {
|
|
|
339
336
|
}
|
|
340
337
|
}
|
|
341
338
|
async #emitPlatformEvent(name, event) {
|
|
342
|
-
await this.
|
|
339
|
+
await this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
343
340
|
this.#logger.warn(formatCompact({
|
|
344
341
|
op: 'slack_platform_event_failed',
|
|
345
342
|
event: name,
|
|
@@ -347,6 +344,18 @@ export class SlackEndpoint extends Endpoint {
|
|
|
347
344
|
}));
|
|
348
345
|
});
|
|
349
346
|
}
|
|
347
|
+
#awaitSocketAck(ack) {
|
|
348
|
+
const signal = this.#executionSignal;
|
|
349
|
+
if (!signal)
|
|
350
|
+
return ack;
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
const abort = () => reject(signal.reason);
|
|
353
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
354
|
+
void ack.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
355
|
+
if (signal.aborted)
|
|
356
|
+
abort();
|
|
357
|
+
});
|
|
358
|
+
}
|
|
350
359
|
async #startSocket() {
|
|
351
360
|
const { config } = this.#options;
|
|
352
361
|
if (!config.appToken) {
|
|
@@ -371,15 +380,57 @@ export class SlackEndpoint extends Endpoint {
|
|
|
371
380
|
return; release(); handle.notifyClosed('Slack socket disconnected'); });
|
|
372
381
|
socket.on('error', error => { connectionError = slackSocketConnectionError(error); if (!active)
|
|
373
382
|
return; release(); handle.notifyClosed('Slack socket error'); });
|
|
374
|
-
socket.on('slack_event',
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
+
socket.on('slack_event', ({ ack, body }) => {
|
|
384
|
+
if (!active || this.#socket !== socket)
|
|
385
|
+
return;
|
|
386
|
+
void this[endpointSpec].events.run(async () => {
|
|
387
|
+
try {
|
|
388
|
+
await this.#awaitSocketAck(ack());
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (!this.#executionSignal?.aborted)
|
|
392
|
+
this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
this.#executionSignal?.throwIfAborted();
|
|
396
|
+
if (active)
|
|
397
|
+
this.handleEnvelope(body);
|
|
398
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
399
|
+
});
|
|
400
|
+
socket.on('interactive', ({ ack, body }) => {
|
|
401
|
+
if (!active || this.#socket !== socket)
|
|
402
|
+
return;
|
|
403
|
+
void this[endpointSpec].events.run(async () => {
|
|
404
|
+
try {
|
|
405
|
+
await this.#awaitSocketAck(ack());
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (!this.#executionSignal?.aborted)
|
|
409
|
+
this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
this.#executionSignal?.throwIfAborted();
|
|
413
|
+
if (active)
|
|
414
|
+
this.admitInteraction(body);
|
|
415
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
416
|
+
});
|
|
417
|
+
socket.on('slash_commands', ({ ack, body }) => {
|
|
418
|
+
if (!active || this.#socket !== socket)
|
|
419
|
+
return;
|
|
420
|
+
void this[endpointSpec].events.run(async () => {
|
|
421
|
+
try {
|
|
422
|
+
await this.#awaitSocketAck(ack());
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (!this.#executionSignal?.aborted)
|
|
426
|
+
this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
this.#executionSignal?.throwIfAborted();
|
|
430
|
+
if (active)
|
|
431
|
+
this.admitSlashCommand(body);
|
|
432
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
433
|
+
});
|
|
383
434
|
try {
|
|
384
435
|
await socket.start();
|
|
385
436
|
}
|
|
@@ -396,17 +447,18 @@ function createSlackEndpointManagement(requireClient) {
|
|
|
396
447
|
return Object.freeze({
|
|
397
448
|
// Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
|
|
398
449
|
// Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
|
|
399
|
-
async listGroups() {
|
|
450
|
+
async listGroups(request = {}) {
|
|
451
|
+
const operation = createEndpointOperation(request);
|
|
400
452
|
const client = requireClient();
|
|
401
453
|
const groups = [];
|
|
402
454
|
let cursor;
|
|
403
455
|
do {
|
|
404
|
-
const page = await client.conversations.list({
|
|
456
|
+
const page = await operation.run(() => client.conversations.list({
|
|
405
457
|
types: 'public_channel',
|
|
406
458
|
exclude_archived: true,
|
|
407
459
|
limit: SLACK_LIST_PAGE_SIZE,
|
|
408
460
|
...(cursor ? { cursor } : {}),
|
|
409
|
-
});
|
|
461
|
+
}));
|
|
410
462
|
for (const value of page.channels ?? []) {
|
|
411
463
|
const channel = asRecord(value);
|
|
412
464
|
const id = typeof channel.id === 'string' ? channel.id : '';
|
|
@@ -419,15 +471,16 @@ function createSlackEndpointManagement(requireClient) {
|
|
|
419
471
|
return groups;
|
|
420
472
|
},
|
|
421
473
|
// Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
|
|
422
|
-
async listFriends() {
|
|
474
|
+
async listFriends(request = {}) {
|
|
475
|
+
const operation = createEndpointOperation(request);
|
|
423
476
|
const client = requireClient();
|
|
424
477
|
const friends = [];
|
|
425
478
|
let cursor;
|
|
426
479
|
do {
|
|
427
|
-
const page = await client.users.list({
|
|
480
|
+
const page = await operation.run(() => client.users.list({
|
|
428
481
|
limit: SLACK_LIST_PAGE_SIZE,
|
|
429
482
|
...(cursor ? { cursor } : {}),
|
|
430
|
-
});
|
|
483
|
+
}));
|
|
431
484
|
for (const value of page.members ?? []) {
|
|
432
485
|
const user = asRecord(value);
|
|
433
486
|
const id = typeof user.id === 'string' ? user.id : '';
|
|
@@ -445,9 +498,10 @@ function createSlackEndpointManagement(requireClient) {
|
|
|
445
498
|
return friends;
|
|
446
499
|
},
|
|
447
500
|
// conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
|
|
448
|
-
async listGroupMembers(groupId) {
|
|
501
|
+
async listGroupMembers({ groupId, signal }) {
|
|
502
|
+
const operation = createEndpointOperation({ signal });
|
|
449
503
|
const client = requireClient();
|
|
450
|
-
const result = await client.conversations.members({ channel: groupId });
|
|
504
|
+
const result = await operation.run(() => client.conversations.members({ channel: groupId }));
|
|
451
505
|
return result.members ?? [];
|
|
452
506
|
},
|
|
453
507
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
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
2
|
export { SlackEndpoint, type SlackEndpointOptions, type SlackSocketLike, type SlackWebClientLike, } from './endpoint.js';
|
|
3
3
|
export { registerSlackWebhookRoutes, handleSlackWebhookRequest, type SlackWebhookHandler, } from './webhook.js';
|
|
4
|
-
export {
|
|
4
|
+
export { type SlackClientEventMap, type SlackUserInfo, } from './client.js';
|
|
5
5
|
export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
|
|
6
6
|
export { normalizeSlackReactionName } from './slack-reaction.js';
|
|
7
7
|
export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
|
|
8
8
|
export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
9
9
|
export { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
|
|
10
10
|
export { editSlackContent, sendSlackContent } from './slack-outbound.js';
|
|
11
|
+
export { SlackAdapter } from './adapter.js';
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { formatInboundContent, formatInteractionContent, formatOutboundWire, formatSlashContent, headerValue, keyboardToBlockKitBlocks, normalizeWebhookPath, readTextBody, resolveSlackChannelType, resolveSlackConfig, slackInboundConversation, verifySlackSignature, } from './protocol.js';
|
|
2
2
|
export { SlackEndpoint, } from './endpoint.js';
|
|
3
3
|
export { registerSlackWebhookRoutes, handleSlackWebhookRequest, } from './webhook.js';
|
|
4
|
-
export { slackClient, } from './client.js';
|
|
5
4
|
export { checkSlackPlatformPermit, normalizeSlackSenderForPermit, platformPermit, slackGroupPermitResolver, } from './platform-permit.js';
|
|
6
5
|
export { normalizeSlackReactionName } from './slack-reaction.js';
|
|
7
6
|
export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
|
|
8
7
|
export { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
9
8
|
export { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
|
|
10
9
|
export { editSlackContent, sendSlackContent } from './slack-outbound.js';
|
|
10
|
+
export { SlackAdapter } from './adapter.js';
|