@zhin.js/adapter-slack 1.1.18 → 1.1.20
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 +45 -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/src/endpoint.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import { WebClient, type WebClientOptions } from '@slack/web-api';
|
|
4
5
|
import { SocketModeClient } from '@slack/socket-mode';
|
|
5
6
|
import { createSlackWebApiAgent } from './web-api-proxy.js';
|
|
6
7
|
import { createSlackStreamAgent } from './stream-proxy.js';
|
|
7
8
|
import { slackSocketConnectionError } from './socket-error.js';
|
|
8
|
-
import {
|
|
9
|
+
import { slackWebClientOptions, slackDeliveryError } from './web-client.js';
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
+
createEndpointSpecification,
|
|
12
|
+
endpointSpec,
|
|
13
|
+
type Endpoint,
|
|
14
|
+
type EndpointSpecification,
|
|
11
15
|
createEndpointLifecycle,
|
|
12
16
|
type EndpointFriend,
|
|
13
17
|
type EndpointGroup,
|
|
14
18
|
type EndpointControl,
|
|
15
19
|
type EndpointManagement,
|
|
16
20
|
type EndpointSendRequest,
|
|
21
|
+
type EndpointManagementRequest,
|
|
22
|
+
createEndpointOperation,
|
|
23
|
+
type EndpointGroupRequest,
|
|
17
24
|
} from 'zhin.js/adapter';
|
|
18
25
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
19
26
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
@@ -38,7 +45,7 @@ import {
|
|
|
38
45
|
import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
|
|
39
46
|
import { normalizeSlackReactionName } from './slack-reaction.js';
|
|
40
47
|
import { postSlackEphemeral } from './slack-response-url.js';
|
|
41
|
-
import { editSlackContent, sendSlackContent, type SlackChatClient } from './slack-outbound.js';
|
|
48
|
+
import { editSlackContent, sendSlackContent, type SlackChatClient, type SlackEditRequest } from './slack-outbound.js';
|
|
42
49
|
import { registerSlackWebhookRoutes, type SlackWebhookHandler } from './webhook.js';
|
|
43
50
|
import { receiveSlackSideEvent } from './side-event-dispatch.js';
|
|
44
51
|
|
|
@@ -51,52 +58,14 @@ export interface SlackSocketLike {
|
|
|
51
58
|
disconnect(): Promise<void>;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export
|
|
55
|
-
auth: {
|
|
56
|
-
test(): Promise<{ user_id?: string; user?: string }>;
|
|
57
|
-
};
|
|
58
|
-
conversations: {
|
|
59
|
-
invite(opts: { channel: string; users: string }): Promise<unknown>;
|
|
60
|
-
kick(opts: { channel: string; user: string }): Promise<unknown>;
|
|
61
|
-
setTopic(opts: { channel: string; topic: string }): Promise<unknown>;
|
|
62
|
-
setPurpose(opts: { channel: string; purpose: string }): Promise<unknown>;
|
|
63
|
-
archive(opts: { channel: string }): Promise<unknown>;
|
|
64
|
-
unarchive(opts: { channel: string }): Promise<unknown>;
|
|
65
|
-
rename(opts: { channel: string; name: string }): Promise<unknown>;
|
|
66
|
-
members(opts: { channel: string }): Promise<{ members?: string[] }>;
|
|
67
|
-
info(opts: { channel: string }): Promise<{ channel?: unknown }>;
|
|
68
|
-
list(opts?: {
|
|
69
|
-
types?: string;
|
|
70
|
-
limit?: number;
|
|
71
|
-
cursor?: string;
|
|
72
|
-
exclude_archived?: boolean;
|
|
73
|
-
}): Promise<{ channels?: unknown[]; response_metadata?: { next_cursor?: string } }>;
|
|
74
|
-
};
|
|
75
|
-
users: {
|
|
76
|
-
info(opts: { user: string }): Promise<{ user?: unknown }>;
|
|
77
|
-
list(opts?: {
|
|
78
|
-
limit?: number;
|
|
79
|
-
cursor?: string;
|
|
80
|
-
}): Promise<{ members?: unknown[]; response_metadata?: { next_cursor?: string } }>;
|
|
81
|
-
};
|
|
82
|
-
reactions: {
|
|
83
|
-
add(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
|
|
84
|
-
remove(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
|
|
85
|
-
};
|
|
86
|
-
pins: {
|
|
87
|
-
add(opts: { channel: string; timestamp: string }): Promise<unknown>;
|
|
88
|
-
remove(opts: { channel: string; timestamp: string }): Promise<unknown>;
|
|
89
|
-
};
|
|
90
|
-
chat: SlackChatClient['chat'] & {
|
|
91
|
-
delete(opts: { channel: string; ts: string }): Promise<unknown>;
|
|
92
|
-
};
|
|
93
|
-
}
|
|
61
|
+
export type SlackWebClientLike = WebClient;
|
|
94
62
|
|
|
95
63
|
export interface SlackEndpointOptions {
|
|
96
64
|
readonly id: CapabilityId;
|
|
97
65
|
readonly config: ResolvedSlackConfig;
|
|
98
66
|
readonly http?: HttpHost;
|
|
99
|
-
readonly
|
|
67
|
+
readonly webApiAgent?: ReturnType<typeof createSlackWebApiAgent>;
|
|
68
|
+
readonly webOptions?: WebClientOptions;
|
|
100
69
|
readonly createSocket?: (opts: {
|
|
101
70
|
readonly appToken: string;
|
|
102
71
|
readonly clientPingTimeout: number;
|
|
@@ -104,52 +73,58 @@ export interface SlackEndpointOptions {
|
|
|
104
73
|
}) => SlackSocketLike;
|
|
105
74
|
}
|
|
106
75
|
|
|
107
|
-
export class SlackEndpoint extends
|
|
76
|
+
export class SlackEndpoint extends WebClient implements Endpoint, SlackWebhookHandler {
|
|
77
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
108
78
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
109
79
|
|
|
110
80
|
readonly #options: SlackEndpointOptions;
|
|
111
81
|
readonly #inboundFilter = createSlackInboundFilterState();
|
|
112
82
|
readonly #messageChannelMap = new Map<string, string>();
|
|
113
|
-
#client?: SlackWebClientLike;
|
|
114
|
-
#webApiAgent?: ReturnType<typeof createSlackWebApiAgent>;
|
|
115
83
|
#socket?: SlackSocketLike;
|
|
116
84
|
readonly #lifecycle: ReturnType<typeof createEndpointLifecycle>;
|
|
117
85
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
118
86
|
#botUserId?: string;
|
|
119
|
-
#
|
|
87
|
+
#executionSignal?: AbortSignal;
|
|
120
88
|
#started = false;
|
|
121
|
-
readonly management: EndpointManagement = createSlackEndpointManagement(() => this
|
|
122
|
-
readonly control: EndpointControl = Object.freeze<EndpointControl>({
|
|
123
|
-
recall: async (message) => {
|
|
89
|
+
readonly #management: EndpointManagement = createSlackEndpointManagement(() => this);
|
|
90
|
+
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
91
|
+
recall: async ({ message, signal }) => {
|
|
124
92
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
125
|
-
if (!ref || !this.#
|
|
126
|
-
await this
|
|
93
|
+
if (!ref || !this.#started) return;
|
|
94
|
+
await createEndpointOperation({ signal }).run(() => this.chat.delete(ref));
|
|
127
95
|
},
|
|
128
|
-
edit: async (message, content) => {
|
|
96
|
+
edit: async ({ message, content, signal }) => {
|
|
129
97
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
130
98
|
if (!ref) return null;
|
|
131
|
-
await this.#editMessage(ref.channel, ref.ts, content);
|
|
99
|
+
await this.#editMessage({ channel: ref.channel, ts: ref.ts, content, signal });
|
|
132
100
|
return message.id;
|
|
133
101
|
},
|
|
134
|
-
addReaction: async (message, emoji) => {
|
|
102
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
135
103
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
136
104
|
if (!ref) return null;
|
|
137
105
|
const reaction = normalizeSlackReactionName(emoji);
|
|
138
|
-
await this.#addReaction(ref.channel, ref.ts, reaction);
|
|
106
|
+
await this.#addReaction({ channel: ref.channel, timestamp: ref.ts, name: reaction, signal });
|
|
139
107
|
return reaction;
|
|
140
108
|
},
|
|
141
|
-
removeReaction: async (message, reactionId) => {
|
|
109
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
142
110
|
const ref = this.resolveMessageRef(message.id, message.conversation.id);
|
|
143
111
|
if (!ref) return;
|
|
144
|
-
await this.#removeReaction(ref.channel, ref.ts, reactionId);
|
|
112
|
+
await this.#removeReaction({ channel: ref.channel, timestamp: ref.ts, name: reactionId, signal });
|
|
145
113
|
},
|
|
146
114
|
});
|
|
147
115
|
|
|
148
116
|
constructor(options: SlackEndpointOptions) {
|
|
149
|
-
super();
|
|
117
|
+
super(options.config.token, slackWebClientOptions({ ...options.webOptions, ...(options.webApiAgent ? { agent: options.webApiAgent } : {}) }));
|
|
150
118
|
this.#logger = getAdapterLogger('slack', options.config.id);
|
|
151
119
|
this.#options = options;
|
|
152
120
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
|
|
121
|
+
const endpoint = this;
|
|
122
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
123
|
+
start: signal => this.#startFramework(signal), open: () => undefined, close: () => undefined,
|
|
124
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
125
|
+
name: options.config.id, get transportState() { return endpoint.#transportState; },
|
|
126
|
+
management: this.#management, control: this.#control,
|
|
127
|
+
});
|
|
153
128
|
}
|
|
154
129
|
|
|
155
130
|
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
@@ -157,12 +132,7 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
157
132
|
return this.#options.config.id;
|
|
158
133
|
}
|
|
159
134
|
|
|
160
|
-
get
|
|
161
|
-
if (!this.#client) throw new Error('Slack client not connected');
|
|
162
|
-
return this.#client;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
get transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
|
|
135
|
+
get #transportState() { return this.config.mode === 'socket' ? this.#lifecycle.state : this.#started ? 'open' : 'stopped'; }
|
|
166
136
|
|
|
167
137
|
get platformUserId(): string | undefined {
|
|
168
138
|
return this.#botUserId;
|
|
@@ -172,14 +142,12 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
172
142
|
return this.#options.config;
|
|
173
143
|
}
|
|
174
144
|
|
|
175
|
-
async
|
|
145
|
+
async #startFramework(signal: AbortSignal): Promise<void> {
|
|
146
|
+
this.#executionSignal = signal;
|
|
176
147
|
if (this.#started) return;
|
|
177
148
|
this.#started = true;
|
|
178
149
|
try {
|
|
179
150
|
const { config } = this.#options;
|
|
180
|
-
if (config.webApiProxy && !this.#options.createClient) this.#webApiAgent = createSlackWebApiAgent(config.webApiProxy);
|
|
181
|
-
this.#client = this.#options.createClient?.(config.token)
|
|
182
|
-
?? (createSlackWebClient(config.token, this.#webApiAgent ? { agent: this.#webApiAgent } : {}) as unknown as SlackWebClientLike);
|
|
183
151
|
|
|
184
152
|
if (config.mode === 'socket') {
|
|
185
153
|
await this.#startSocket();
|
|
@@ -196,7 +164,7 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
196
164
|
}));
|
|
197
165
|
}
|
|
198
166
|
|
|
199
|
-
const authTest = await this
|
|
167
|
+
const authTest = await this.auth.test();
|
|
200
168
|
if (authTest.user_id) this.#botUserId = String(authTest.user_id);
|
|
201
169
|
|
|
202
170
|
this.#logger.info(
|
|
@@ -204,24 +172,14 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
204
172
|
+ (this.#botUserId ? ` | user: ${this.#botUserId}` : ''),
|
|
205
173
|
);
|
|
206
174
|
} catch (error) {
|
|
207
|
-
await this
|
|
175
|
+
await this.#stopFramework();
|
|
208
176
|
this.#logger.error('Failed to connect Slack endpoint:', error);
|
|
209
177
|
throw error;
|
|
210
178
|
}
|
|
211
179
|
}
|
|
212
180
|
|
|
213
|
-
|
|
214
|
-
this.#
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
close(): void {
|
|
218
|
-
this.#open = false;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async stop(): Promise<void> {
|
|
222
|
-
this.#open = false;
|
|
223
|
-
this.#webApiAgent?.destroy();
|
|
224
|
-
this.#webApiAgent = undefined;
|
|
181
|
+
async #stopFramework(): Promise<void> {
|
|
182
|
+
this.#options.webApiAgent?.destroy();
|
|
225
183
|
await this.#lifecycle.stop();
|
|
226
184
|
if (this.#socket) {
|
|
227
185
|
try {
|
|
@@ -232,148 +190,153 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
232
190
|
this.#socket = undefined;
|
|
233
191
|
}
|
|
234
192
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
235
|
-
this.#client = undefined;
|
|
236
193
|
this.#started = false;
|
|
237
194
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
238
195
|
}
|
|
239
196
|
|
|
240
|
-
async
|
|
241
|
-
if (!this.#
|
|
197
|
+
async #sendFramework({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
198
|
+
if (!this.#started) throw new Error('Slack client not connected');
|
|
242
199
|
const channel = conversation.id;
|
|
243
200
|
const threadTs = conversation.threadId;
|
|
244
201
|
try {
|
|
245
|
-
|
|
246
|
-
this
|
|
247
|
-
|
|
248
|
-
{ channel, threadTs },
|
|
249
|
-
this.#logger,
|
|
250
|
-
);
|
|
251
|
-
this.trackMessageChannel(result.ts, channel);
|
|
252
|
-
return formatSlackMessageRef(channel, result.ts);
|
|
202
|
+
const result = await sendSlackContent(this, { content: payload, channel, threadTs, logger: this.#logger, signal });
|
|
203
|
+
this.trackMessageChannel(result.ts, channel);
|
|
204
|
+
return formatSlackMessageRef(channel, result.ts);
|
|
253
205
|
} catch (error) { throw slackDeliveryError(error); }
|
|
254
206
|
}
|
|
255
207
|
|
|
256
208
|
/** Test / internal: admit a message event when open. */
|
|
257
|
-
admit(event: SlackMessageEvent | SlackEvent): void {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
209
|
+
admit(event: SlackMessageEvent | SlackEvent, original: unknown = event): void {
|
|
210
|
+
void this[endpointSpec].events.run(() => {
|
|
211
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
212
|
+
if (event.type !== 'message' && event.type !== 'app_mention') return;
|
|
213
|
+
const msg = event as SlackMessageEvent;
|
|
214
|
+
if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
|
|
215
|
+
if (!msg.channel || !msg.ts) return;
|
|
216
|
+
|
|
217
|
+
this.trackMessageChannel(msg.ts, msg.channel);
|
|
218
|
+
const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
|
|
219
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
220
|
+
channelId: msg.channel,
|
|
221
|
+
channelType: msg.channel_type,
|
|
222
|
+
threadId: threadTs,
|
|
223
|
+
});
|
|
224
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
225
|
+
conversation,
|
|
226
|
+
event: original,
|
|
227
|
+
...(typeof msg.ts === 'string' && Number.isFinite(Number(msg.ts)) ? { platformSentAt: Number(msg.ts) * 1_000 } : {}),
|
|
228
|
+
message: { conversation, id: msg.ts },
|
|
229
|
+
content: formatInboundContent(msg),
|
|
230
|
+
sender: { id: msg.user ?? msg.channel ?? '' },
|
|
231
|
+
endpointId: this.#options.config.id,
|
|
232
|
+
...(msg.type === 'app_mention' ? { mentioned: true } : {}),
|
|
233
|
+
metadata: Object.freeze({
|
|
234
|
+
channelType: resolveSlackChannelType(msg),
|
|
235
|
+
userId: msg.user,
|
|
236
|
+
threadTs,
|
|
237
|
+
ts: msg.ts,
|
|
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
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
292
247
|
}
|
|
293
248
|
|
|
294
249
|
admitInteraction(payload: SlackInteractionPayload): void {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
250
|
+
void this[endpointSpec].events.run(() => {
|
|
251
|
+
void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
|
|
252
|
+
if (payload.type !== 'block_actions' || !payload.actions?.length) return;
|
|
253
|
+
const channelId = payload.channel?.id ?? '';
|
|
254
|
+
const userId = payload.user.id;
|
|
255
|
+
const messageTs = payload.message?.ts ?? '';
|
|
256
|
+
const sourceThread = payload.message?.thread_ts;
|
|
257
|
+
const threadTs = typeof sourceThread === 'string' && sourceThread && sourceThread !== messageTs ? sourceThread : undefined;
|
|
258
|
+
if (payload.response_url) {
|
|
259
|
+
postSlackEphemeral(payload.response_url, '已收到', this.#logger);
|
|
260
|
+
}
|
|
261
|
+
const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
|
|
262
|
+
const platformSentAt = Number(payload.actions[0]?.action_ts) * 1_000;
|
|
263
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
264
|
+
channelId: channelId || userId,
|
|
265
|
+
// block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
|
|
266
|
+
channelType: channelId ? undefined : 'im',
|
|
267
|
+
threadId: threadTs,
|
|
268
|
+
});
|
|
269
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
270
|
+
conversation,
|
|
271
|
+
event: payload,
|
|
272
|
+
...(Number.isFinite(platformSentAt) ? { platformSentAt } : {}),
|
|
273
|
+
message: { conversation, id: actionTs },
|
|
274
|
+
content: formatInteractionContent(payload),
|
|
275
|
+
segments: [{ type: 'action', data: { id: actionTs, payload: payload.actions[0].value ?? payload.actions[0].action_id, ...(messageTs ? { sourceMessageId: messageTs } : {}) } }],
|
|
276
|
+
sender: { id: userId },
|
|
277
|
+
endpointId: this.#options.config.id,
|
|
278
|
+
metadata: Object.freeze({
|
|
279
|
+
eventType: 'block_actions',
|
|
280
|
+
actionId: payload.actions[0]?.action_id,
|
|
281
|
+
threadTs,
|
|
282
|
+
}),
|
|
283
|
+
}).catch((err) => {
|
|
284
|
+
this.#logger.warn(formatCompact({
|
|
285
|
+
op: 'slack_gateway_receive_failed',
|
|
286
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
287
|
+
error: err instanceof Error ? err.message : String(err),
|
|
288
|
+
}));
|
|
289
|
+
});
|
|
290
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
332
291
|
}
|
|
333
292
|
|
|
334
293
|
admitSlashCommand(cmd: SlackSlashCommand): void {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
294
|
+
void this[endpointSpec].events.run(() => {
|
|
295
|
+
void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
|
|
296
|
+
postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
|
|
297
|
+
const conversation = slackInboundConversation(String(this.#options.id), {
|
|
298
|
+
channelId: cmd.channel_id,
|
|
299
|
+
});
|
|
300
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
301
|
+
conversation,
|
|
302
|
+
message: { conversation, id: cmd.trigger_id },
|
|
303
|
+
event: cmd,
|
|
304
|
+
content: formatSlashContent(cmd),
|
|
305
|
+
sender: { id: cmd.user_id, name: cmd.user_name },
|
|
306
|
+
endpointId: this.#options.config.id,
|
|
307
|
+
metadata: Object.freeze({
|
|
308
|
+
eventType: 'slash_command',
|
|
309
|
+
command: cmd.command,
|
|
310
|
+
}),
|
|
311
|
+
}).catch((err) => {
|
|
312
|
+
this.#logger.warn(formatCompact({
|
|
313
|
+
op: 'slack_gateway_receive_failed',
|
|
314
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
315
|
+
error: err instanceof Error ? err.message : String(err),
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
358
319
|
}
|
|
359
320
|
|
|
360
321
|
handleEnvelope(body: unknown): void {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
322
|
+
void this[endpointSpec].events.run(() => {
|
|
323
|
+
const envelope = body as SlackEventEnvelope;
|
|
324
|
+
if (envelope?.type === 'event_callback' && envelope.event) {
|
|
325
|
+
const event = envelope.event;
|
|
326
|
+
if (event.type !== 'message' && event.type !== 'app_mention') {
|
|
327
|
+
void this.#emitPlatformEvent(event.type || 'event', event);
|
|
328
|
+
receiveSlackSideEvent(
|
|
329
|
+
(name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
330
|
+
String(this.#options.id),
|
|
331
|
+
this.#options.config.id,
|
|
332
|
+
event,
|
|
333
|
+
this.#logger,
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.admit(event, body);
|
|
374
338
|
}
|
|
375
|
-
|
|
376
|
-
}
|
|
339
|
+
}).catch(() => this.#logger.debug('Inbound callback did not complete'));
|
|
377
340
|
}
|
|
378
341
|
|
|
379
342
|
trackMessageChannel(ts: string, channel: string): void {
|
|
@@ -399,21 +362,21 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
399
362
|
}
|
|
400
363
|
|
|
401
364
|
async recallMessage(messageId: string): Promise<void> {
|
|
402
|
-
if (!this.#
|
|
365
|
+
if (!this.#started) return;
|
|
403
366
|
const ref = this.resolveMessageRef(messageId);
|
|
404
367
|
if (!ref) return;
|
|
405
|
-
await this
|
|
368
|
+
await this.chat.delete({ channel: ref.channel, ts: ref.ts });
|
|
406
369
|
}
|
|
407
370
|
|
|
408
|
-
async #editMessage(
|
|
409
|
-
if (!this.#
|
|
410
|
-
await editSlackContent(this
|
|
371
|
+
async #editMessage(request: SlackEditRequest): Promise<void> {
|
|
372
|
+
if (!this.#started) throw new Error('Slack client not connected');
|
|
373
|
+
await editSlackContent(this, request);
|
|
411
374
|
}
|
|
412
375
|
|
|
413
|
-
async #addReaction(channel
|
|
376
|
+
async #addReaction({ channel, timestamp, name, signal }: SlackReactionRequest): Promise<void> {
|
|
414
377
|
const reaction = normalizeSlackReactionName(name);
|
|
415
378
|
try {
|
|
416
|
-
await this
|
|
379
|
+
await createEndpointOperation({ signal }).run(() => this.reactions.add({ channel, timestamp, name: reaction }));
|
|
417
380
|
} catch (error) {
|
|
418
381
|
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
419
382
|
if (code === 'already_reacted') return;
|
|
@@ -421,10 +384,10 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
421
384
|
}
|
|
422
385
|
}
|
|
423
386
|
|
|
424
|
-
async #removeReaction(channel
|
|
387
|
+
async #removeReaction({ channel, timestamp, name, signal }: SlackReactionRequest): Promise<void> {
|
|
425
388
|
const reaction = normalizeSlackReactionName(name);
|
|
426
389
|
try {
|
|
427
|
-
await this
|
|
390
|
+
await createEndpointOperation({ signal }).run(() => this.reactions.remove({ channel, timestamp, name: reaction }));
|
|
428
391
|
} catch (error) {
|
|
429
392
|
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
430
393
|
if (code === 'no_reaction') return;
|
|
@@ -433,7 +396,7 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
433
396
|
}
|
|
434
397
|
|
|
435
398
|
async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
|
|
436
|
-
await this.
|
|
399
|
+
await this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
437
400
|
this.#logger.warn(formatCompact({
|
|
438
401
|
op: 'slack_platform_event_failed',
|
|
439
402
|
event: name,
|
|
@@ -442,6 +405,17 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
442
405
|
});
|
|
443
406
|
}
|
|
444
407
|
|
|
408
|
+
#awaitSocketAck(ack: Promise<unknown>): Promise<unknown> {
|
|
409
|
+
const signal = this.#executionSignal;
|
|
410
|
+
if (!signal) return ack;
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const abort = () => reject(signal.reason);
|
|
413
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
414
|
+
void ack.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
415
|
+
if (signal.aborted) abort();
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
445
419
|
async #startSocket(): Promise<void> {
|
|
446
420
|
const { config } = this.#options;
|
|
447
421
|
if (!config.appToken) {
|
|
@@ -462,9 +436,39 @@ export class SlackEndpoint extends Endpoint<SlackWebClientLike> implements Slack
|
|
|
462
436
|
handle.onForceClose(release);
|
|
463
437
|
socket.on('disconnected', () => { if (!active) return; release(); handle.notifyClosed('Slack socket disconnected'); });
|
|
464
438
|
socket.on('error', error => { connectionError = slackSocketConnectionError(error); if (!active) return; release(); handle.notifyClosed('Slack socket error'); });
|
|
465
|
-
socket.on('slack_event',
|
|
466
|
-
|
|
467
|
-
|
|
439
|
+
socket.on('slack_event', ({ ack, body }) => {
|
|
440
|
+
if (!active || this.#socket !== socket) return;
|
|
441
|
+
void this[endpointSpec].events.run(async () => {
|
|
442
|
+
try { await this.#awaitSocketAck(ack()); } catch (error) {
|
|
443
|
+
if (!this.#executionSignal?.aborted) this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
this.#executionSignal?.throwIfAborted();
|
|
447
|
+
if (active) this.handleEnvelope(body);
|
|
448
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
449
|
+
});
|
|
450
|
+
socket.on('interactive', ({ ack, body }) => {
|
|
451
|
+
if (!active || this.#socket !== socket) return;
|
|
452
|
+
void this[endpointSpec].events.run(async () => {
|
|
453
|
+
try { await this.#awaitSocketAck(ack()); } catch (error) {
|
|
454
|
+
if (!this.#executionSignal?.aborted) this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
this.#executionSignal?.throwIfAborted();
|
|
458
|
+
if (active) this.admitInteraction(body as SlackInteractionPayload);
|
|
459
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
460
|
+
});
|
|
461
|
+
socket.on('slash_commands', ({ ack, body }) => {
|
|
462
|
+
if (!active || this.#socket !== socket) return;
|
|
463
|
+
void this[endpointSpec].events.run(async () => {
|
|
464
|
+
try { await this.#awaitSocketAck(ack()); } catch (error) {
|
|
465
|
+
if (!this.#executionSignal?.aborted) this.#logger.warn(formatCompact({ op: 'slack_socket_ack_failed' }));
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
this.#executionSignal?.throwIfAborted();
|
|
469
|
+
if (active) this.admitSlashCommand(body as SlackSlashCommand);
|
|
470
|
+
}).catch(() => this.#logger.debug('Socket callback did not complete'));
|
|
471
|
+
});
|
|
468
472
|
try { await socket.start(); } catch (error) { release(); throw connectionError ?? slackSocketConnectionError(error); }
|
|
469
473
|
});
|
|
470
474
|
}
|
|
@@ -479,17 +483,18 @@ function createSlackEndpointManagement(
|
|
|
479
483
|
return Object.freeze<EndpointManagement>({
|
|
480
484
|
// Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
|
|
481
485
|
// Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
|
|
482
|
-
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
486
|
+
async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
|
|
487
|
+
const operation = createEndpointOperation(request);
|
|
483
488
|
const client = requireClient();
|
|
484
489
|
const groups: EndpointGroup[] = [];
|
|
485
490
|
let cursor: string | undefined;
|
|
486
491
|
do {
|
|
487
|
-
const page = await client.conversations.list({
|
|
492
|
+
const page = await operation.run(() => client.conversations.list({
|
|
488
493
|
types: 'public_channel',
|
|
489
494
|
exclude_archived: true,
|
|
490
495
|
limit: SLACK_LIST_PAGE_SIZE,
|
|
491
496
|
...(cursor ? { cursor } : {}),
|
|
492
|
-
});
|
|
497
|
+
}));
|
|
493
498
|
for (const value of page.channels ?? []) {
|
|
494
499
|
const channel = asRecord(value);
|
|
495
500
|
const id = typeof channel.id === 'string' ? channel.id : '';
|
|
@@ -501,15 +506,16 @@ function createSlackEndpointManagement(
|
|
|
501
506
|
return groups;
|
|
502
507
|
},
|
|
503
508
|
// Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
|
|
504
|
-
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
509
|
+
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
510
|
+
const operation = createEndpointOperation(request);
|
|
505
511
|
const client = requireClient();
|
|
506
512
|
const friends: EndpointFriend[] = [];
|
|
507
513
|
let cursor: string | undefined;
|
|
508
514
|
do {
|
|
509
|
-
const page = await client.users.list({
|
|
515
|
+
const page = await operation.run(() => client.users.list({
|
|
510
516
|
limit: SLACK_LIST_PAGE_SIZE,
|
|
511
517
|
...(cursor ? { cursor } : {}),
|
|
512
|
-
});
|
|
518
|
+
}));
|
|
513
519
|
for (const value of page.members ?? []) {
|
|
514
520
|
const user = asRecord(value);
|
|
515
521
|
const id = typeof user.id === 'string' ? user.id : '';
|
|
@@ -526,9 +532,10 @@ function createSlackEndpointManagement(
|
|
|
526
532
|
return friends;
|
|
527
533
|
},
|
|
528
534
|
// conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
|
|
529
|
-
async listGroupMembers(groupId:
|
|
535
|
+
async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly string[]> {
|
|
536
|
+
const operation = createEndpointOperation({ signal });
|
|
530
537
|
const client = requireClient();
|
|
531
|
-
const result = await client.conversations.members({ channel: groupId });
|
|
538
|
+
const result = await operation.run(() => client.conversations.members({ channel: groupId }));
|
|
532
539
|
return result.members ?? [];
|
|
533
540
|
},
|
|
534
541
|
});
|
|
@@ -539,3 +546,9 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
539
546
|
? value as Record<string, unknown>
|
|
540
547
|
: {};
|
|
541
548
|
}
|
|
549
|
+
|
|
550
|
+
interface SlackReactionRequest extends EndpointManagementRequest {
|
|
551
|
+
readonly channel: string;
|
|
552
|
+
readonly timestamp: string;
|
|
553
|
+
readonly name: string;
|
|
554
|
+
}
|