@zhin.js/adapter-slack 4.1.0 → 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 +41 -0
- package/README.md +58 -250
- package/adapters/slack.ts +34 -0
- package/agent/tools/add_reaction.ts +2 -2
- package/agent/tools/archive_channel.ts +2 -2
- package/agent/tools/edit_message.ts +5 -4
- package/agent/tools/invite_to_channel.ts +2 -2
- package/agent/tools/pin_message.ts +2 -2
- package/agent/tools/remove_reaction.ts +2 -2
- package/agent/tools/set_purpose.ts +2 -2
- package/agent/tools/set_topic.ts +2 -2
- package/agent/tools/unarchive.ts +2 -2
- package/agent/tools/unpin_message.ts +2 -2
- package/agent/tools/user_info.ts +2 -2
- package/lib/endpoint.d.ts +147 -0
- package/lib/endpoint.js +335 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +11 -0
- 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/{src/mrkdwn-to-markdown.js → mrkdwn-to-markdown.js} +0 -1
- package/lib/platform-permit.d.ts +16 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +1 -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/{src/slack-inbound-filter.js → slack-inbound-filter.js} +0 -1
- package/lib/slack-message-ref.d.ts +7 -0
- package/lib/{src/slack-message-ref.js → slack-message-ref.js} +0 -1
- 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/{src/slack-reaction.js → slack-reaction.js} +0 -1
- package/lib/slack-response-url.d.ts +5 -0
- package/lib/{src/slack-response-url.js → slack-response-url.js} +0 -1
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +69 -0
- package/package.json +41 -20
- package/plugin.ts +13 -0
- package/schema.json +38 -0
- package/src/endpoint.ts +317 -227
- package/src/index.ts +62 -66
- package/src/markdown-to-mrkdwn.ts +63 -4
- package/src/platform-permit.ts +1 -1
- package/src/protocol.ts +380 -0
- package/src/slack-agent-deps.ts +46 -11
- package/src/slack-inbound-filter.ts +1 -1
- package/src/slack-outbound.ts +32 -134
- package/src/webhook.ts +103 -0
- package/lib/agent/tools/add_reaction.js +0 -21
- package/lib/agent/tools/add_reaction.js.map +0 -1
- package/lib/agent/tools/archive_channel.js +0 -21
- package/lib/agent/tools/archive_channel.js.map +0 -1
- package/lib/agent/tools/edit_message.js +0 -22
- package/lib/agent/tools/edit_message.js.map +0 -1
- package/lib/agent/tools/invite_to_channel.js +0 -22
- package/lib/agent/tools/invite_to_channel.js.map +0 -1
- package/lib/agent/tools/pin_message.js +0 -22
- package/lib/agent/tools/pin_message.js.map +0 -1
- package/lib/agent/tools/remove_reaction.js +0 -21
- package/lib/agent/tools/remove_reaction.js.map +0 -1
- package/lib/agent/tools/set_purpose.js +0 -22
- package/lib/agent/tools/set_purpose.js.map +0 -1
- package/lib/agent/tools/set_topic.js +0 -22
- package/lib/agent/tools/set_topic.js.map +0 -1
- package/lib/agent/tools/unarchive.js +0 -21
- package/lib/agent/tools/unarchive.js.map +0 -1
- package/lib/agent/tools/unpin_message.js +0 -22
- package/lib/agent/tools/unpin_message.js.map +0 -1
- package/lib/agent/tools/user_info.js +0 -28
- package/lib/agent/tools/user_info.js.map +0 -1
- package/lib/src/adapter.js +0 -62
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -312
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/event-dispatcher.js +0 -233
- package/lib/src/event-dispatcher.js.map +0 -1
- package/lib/src/index.js +0 -51
- package/lib/src/index.js.map +0 -1
- package/lib/src/markdown-to-mrkdwn.js +0 -61
- package/lib/src/markdown-to-mrkdwn.js.map +0 -1
- package/lib/src/mrkdwn-to-markdown.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/signing.js +0 -21
- package/lib/src/signing.js.map +0 -1
- package/lib/src/slack-agent-deps.js +0 -10
- package/lib/src/slack-agent-deps.js.map +0 -1
- package/lib/src/slack-inbound-filter.js.map +0 -1
- package/lib/src/slack-inbound.js +0 -127
- package/lib/src/slack-inbound.js.map +0 -1
- package/lib/src/slack-message-ref.js.map +0 -1
- package/lib/src/slack-outbound.js +0 -223
- package/lib/src/slack-outbound.js.map +0 -1
- package/lib/src/slack-reaction.js.map +0 -1
- package/lib/src/slack-response-url.js.map +0 -1
- package/lib/src/slack-side-events.js +0 -64
- package/lib/src/slack-side-events.js.map +0 -1
- package/lib/src/transport-http.js +0 -83
- package/lib/src/transport-http.js.map +0 -1
- package/lib/src/transport-socket.js +0 -68
- package/lib/src/transport-socket.js.map +0 -1
- package/lib/src/types.js +0 -5
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -75
- package/src/event-dispatcher.ts +0 -255
- package/src/segment-mapper.ts +0 -1
- package/src/signing.ts +0 -28
- package/src/slack-inbound.ts +0 -140
- package/src/slack-side-events.ts +0 -74
- package/src/transport-http.ts +0 -95
- package/src/transport-socket.ts +0 -77
- package/src/types.ts +0 -113
package/src/endpoint.ts
CHANGED
|
@@ -1,183 +1,294 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
4
5
|
import { WebClient } from '@slack/web-api';
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
6
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
7
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
8
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
9
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
11
|
+
import {
|
|
12
|
+
formatInboundContent,
|
|
13
|
+
formatInteractionContent,
|
|
14
|
+
formatSlashContent,
|
|
15
|
+
inboundMessageId,
|
|
16
|
+
resolveSlackChannelType,
|
|
17
|
+
type ResolvedSlackConfig,
|
|
18
|
+
type SlackEvent,
|
|
19
|
+
type SlackEventEnvelope,
|
|
20
|
+
type SlackInteractionPayload,
|
|
21
|
+
type SlackMessageEvent,
|
|
22
|
+
type SlackSlashCommand,
|
|
23
|
+
} from './protocol.js';
|
|
24
|
+
import { registerSlackAgentEndpoint } from './slack-agent-deps.js';
|
|
25
|
+
import {
|
|
26
|
+
createSlackInboundFilterState,
|
|
27
|
+
shouldDropSlackInboundMessage,
|
|
28
|
+
} from './slack-inbound-filter.js';
|
|
12
29
|
import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
30
|
+
import { normalizeSlackReactionName } from './slack-reaction.js';
|
|
31
|
+
import { postSlackEphemeral } from './slack-response-url.js';
|
|
32
|
+
import { editSlackContent, sendSlackContent, type SlackChatClient } from './slack-outbound.js';
|
|
33
|
+
import { registerSlackWebhookRoutes, type SlackWebhookHandler } from './webhook.js';
|
|
34
|
+
|
|
35
|
+
const logger = getLogger('slack');
|
|
36
|
+
|
|
37
|
+
export interface SlackSocketLike {
|
|
38
|
+
on(
|
|
39
|
+
event: string,
|
|
40
|
+
handler: (args: { ack: () => Promise<void>; body: unknown }) => void | Promise<void>,
|
|
41
|
+
): void;
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
disconnect(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SlackWebClientLike extends SlackChatClient {
|
|
47
|
+
auth: {
|
|
48
|
+
test(): Promise<{ user_id?: string; user?: string }>;
|
|
49
|
+
};
|
|
50
|
+
conversations: {
|
|
51
|
+
invite(opts: { channel: string; users: string }): Promise<unknown>;
|
|
52
|
+
kick(opts: { channel: string; user: string }): Promise<unknown>;
|
|
53
|
+
setTopic(opts: { channel: string; topic: string }): Promise<unknown>;
|
|
54
|
+
setPurpose(opts: { channel: string; purpose: string }): Promise<unknown>;
|
|
55
|
+
archive(opts: { channel: string }): Promise<unknown>;
|
|
56
|
+
unarchive(opts: { channel: string }): Promise<unknown>;
|
|
57
|
+
rename(opts: { channel: string; name: string }): Promise<unknown>;
|
|
58
|
+
members(opts: { channel: string }): Promise<{ members?: string[] }>;
|
|
59
|
+
info(opts: { channel: string }): Promise<{ channel?: unknown }>;
|
|
60
|
+
};
|
|
61
|
+
users: {
|
|
62
|
+
info(opts: { user: string }): Promise<{ user?: unknown }>;
|
|
63
|
+
};
|
|
64
|
+
reactions: {
|
|
65
|
+
add(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
|
|
66
|
+
remove(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
|
|
67
|
+
};
|
|
68
|
+
pins: {
|
|
69
|
+
add(opts: { channel: string; timestamp: string }): Promise<unknown>;
|
|
70
|
+
remove(opts: { channel: string; timestamp: string }): Promise<unknown>;
|
|
71
|
+
};
|
|
72
|
+
chat: SlackChatClient['chat'] & {
|
|
73
|
+
delete(opts: { channel: string; ts: string }): Promise<unknown>;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
16
76
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly
|
|
24
|
-
|
|
77
|
+
export interface SlackEndpointOptions {
|
|
78
|
+
readonly id: CapabilityId;
|
|
79
|
+
readonly gateway: MessageGateway;
|
|
80
|
+
readonly config: ResolvedSlackConfig;
|
|
81
|
+
readonly http?: HttpHost;
|
|
82
|
+
readonly createClient?: (token: string) => SlackWebClientLike;
|
|
83
|
+
readonly createSocket?: (opts: {
|
|
84
|
+
readonly appToken: string;
|
|
85
|
+
readonly clientPingTimeout: number;
|
|
86
|
+
}) => SlackSocketLike;
|
|
87
|
+
}
|
|
25
88
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
89
|
+
export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
|
|
90
|
+
readonly #options: SlackEndpointOptions;
|
|
91
|
+
readonly #inboundFilter = createSlackInboundFilterState();
|
|
92
|
+
readonly #messageChannelMap = new Map<string, string>();
|
|
93
|
+
#client?: SlackWebClientLike;
|
|
94
|
+
#socket?: SlackSocketLike;
|
|
95
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
96
|
+
#botUserId?: string;
|
|
97
|
+
#open = false;
|
|
98
|
+
#started = false;
|
|
99
|
+
#unregisterAgent?: () => void;
|
|
100
|
+
|
|
101
|
+
constructor(options: SlackEndpointOptions) {
|
|
102
|
+
this.#options = options;
|
|
103
|
+
}
|
|
29
104
|
|
|
30
|
-
get
|
|
31
|
-
|
|
105
|
+
get client(): SlackWebClientLike | undefined {
|
|
106
|
+
return this.#client;
|
|
107
|
+
}
|
|
32
108
|
|
|
33
|
-
|
|
34
|
-
this
|
|
109
|
+
get platformUserId(): string | undefined {
|
|
110
|
+
return this.#botUserId;
|
|
35
111
|
}
|
|
36
112
|
|
|
37
|
-
|
|
113
|
+
get config(): ResolvedSlackConfig {
|
|
114
|
+
return this.#options.config;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async start(): Promise<void> {
|
|
118
|
+
if (this.#started) return;
|
|
119
|
+
this.#started = true;
|
|
38
120
|
try {
|
|
39
|
-
|
|
40
|
-
this
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this
|
|
47
|
-
|
|
121
|
+
const { config } = this.#options;
|
|
122
|
+
this.#client = this.#options.createClient?.(config.token)
|
|
123
|
+
?? (new WebClient(config.token) as unknown as SlackWebClientLike);
|
|
124
|
+
|
|
125
|
+
this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
|
|
126
|
+
|
|
127
|
+
if (config.mode === 'socket') {
|
|
128
|
+
await this.#startSocket();
|
|
129
|
+
} else {
|
|
130
|
+
const http = this.#options.http;
|
|
131
|
+
if (!http) {
|
|
132
|
+
throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
|
|
133
|
+
}
|
|
134
|
+
this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
|
|
135
|
+
logger.debug(formatCompact({
|
|
136
|
+
endpoint: config.name,
|
|
137
|
+
op: 'webhook',
|
|
138
|
+
path: config.webhookPath,
|
|
139
|
+
}));
|
|
48
140
|
}
|
|
49
141
|
|
|
50
|
-
|
|
142
|
+
const authTest = await this.#client.auth.test();
|
|
143
|
+
if (authTest.user_id) this.#botUserId = String(authTest.user_id);
|
|
51
144
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
endpoint: this.$config.name,
|
|
58
|
-
user: authTest.user,
|
|
59
|
-
platform_user_id: this.$platformUserId,
|
|
145
|
+
logger.info(formatCompact({
|
|
146
|
+
op: 'connect',
|
|
147
|
+
endpoint: config.name,
|
|
148
|
+
mode: config.mode,
|
|
149
|
+
platform_user_id: this.#botUserId,
|
|
60
150
|
}));
|
|
61
|
-
|
|
62
|
-
if (!this.$config.socketMode && router) {
|
|
63
|
-
this.logger.debug(formatCompact({ op: 'listen', mode: 'http', path: '/slack/events' }));
|
|
64
|
-
}
|
|
65
151
|
} catch (error) {
|
|
66
|
-
this.
|
|
67
|
-
|
|
152
|
+
await this.stop();
|
|
153
|
+
logger.error('Failed to connect Slack endpoint:', error);
|
|
68
154
|
throw error;
|
|
69
155
|
}
|
|
70
156
|
}
|
|
71
157
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
158
|
+
open(): void {
|
|
159
|
+
this.#open = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
close(): void {
|
|
163
|
+
this.#open = false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async stop(): Promise<void> {
|
|
167
|
+
this.#open = false;
|
|
168
|
+
if (this.#socket) {
|
|
169
|
+
try {
|
|
170
|
+
await this.#socket.disconnect();
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore */
|
|
77
173
|
}
|
|
78
|
-
this
|
|
79
|
-
this.$connected = false;
|
|
80
|
-
this.logger.debug(formatCompact({ op: 'disconnect', endpoint: this.$config.name }));
|
|
81
|
-
} catch (error) {
|
|
82
|
-
this.logger.error('Error disconnecting Slack bot:', error);
|
|
83
|
-
throw error;
|
|
174
|
+
this.#socket = undefined;
|
|
84
175
|
}
|
|
176
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
177
|
+
this.#unregisterAgent?.();
|
|
178
|
+
this.#unregisterAgent = undefined;
|
|
179
|
+
this.#client = undefined;
|
|
180
|
+
this.#started = false;
|
|
181
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
85
182
|
}
|
|
86
183
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const result = Message.from(msg, {
|
|
100
|
-
$id: formatSlackMessageRef(channelId, msg.ts),
|
|
101
|
-
$adapter: 'slack',
|
|
102
|
-
$endpoint: this.$config.name,
|
|
103
|
-
$sender: { id: userId, name: userName },
|
|
104
|
-
$channel: { id: channelId, type: channelType },
|
|
105
|
-
$content: content,
|
|
106
|
-
$raw: messageText,
|
|
107
|
-
$timestamp: parseFloat(msg.ts) * 1000,
|
|
108
|
-
$quote_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
|
|
109
|
-
$recall: async () => {
|
|
110
|
-
await this.client!.chat.delete({ channel: channelId, ts: msg.ts });
|
|
111
|
-
},
|
|
112
|
-
$reply: async (replyContent, quote?) => {
|
|
113
|
-
if (!Array.isArray(replyContent)) replyContent = [replyContent];
|
|
114
|
-
const threadTs = quote
|
|
115
|
-
? (typeof quote === 'boolean' ? msg.ts : quote)
|
|
116
|
-
: (msg.thread_ts ?? undefined);
|
|
117
|
-
return await this.adapter.sendMessage({
|
|
118
|
-
context: 'slack',
|
|
119
|
-
endpoint: this.$config.name,
|
|
120
|
-
id: channelId,
|
|
121
|
-
type: channelType,
|
|
122
|
-
content: replyContent,
|
|
123
|
-
...(threadTs ? { threadId: threadTs } : {}),
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
});
|
|
184
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
185
|
+
if (!this.#client) throw new Error('Slack client not connected');
|
|
186
|
+
const { channel, threadTs } = parseSendTarget(target);
|
|
187
|
+
const result = await sendSlackContent(
|
|
188
|
+
this.#client,
|
|
189
|
+
payload,
|
|
190
|
+
{ channel, threadTs },
|
|
191
|
+
logger,
|
|
192
|
+
);
|
|
193
|
+
if (result.ts) this.trackMessageChannel(result.ts, channel);
|
|
194
|
+
return formatSlackMessageRef(channel, result.ts || String(Date.now()));
|
|
195
|
+
}
|
|
127
196
|
|
|
128
|
-
|
|
197
|
+
/** Test / internal: admit a message event when open. */
|
|
198
|
+
admit(event: SlackMessageEvent | SlackEvent): void {
|
|
199
|
+
if (!this.#open) return;
|
|
200
|
+
if (event.type !== 'message' && event.type !== 'app_mention') return;
|
|
201
|
+
const msg = event as SlackMessageEvent;
|
|
202
|
+
if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
|
|
203
|
+
if (!msg.channel || !msg.ts) return;
|
|
204
|
+
|
|
205
|
+
this.trackMessageChannel(msg.ts, msg.channel);
|
|
206
|
+
void this.#options.gateway.receive({
|
|
207
|
+
adapter: this.#options.id,
|
|
208
|
+
target: msg.channel,
|
|
209
|
+
content: formatInboundContent(msg),
|
|
210
|
+
sender: msg.user ?? msg.channel,
|
|
211
|
+
id: inboundMessageId(msg),
|
|
212
|
+
metadata: Object.freeze({
|
|
213
|
+
endpoint: this.#options.config.name,
|
|
214
|
+
channelType: resolveSlackChannelType(msg),
|
|
215
|
+
userId: msg.user,
|
|
216
|
+
threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
|
|
217
|
+
ts: msg.ts,
|
|
218
|
+
// app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
219
|
+
...(msg.type === 'app_mention' ? { mentioned: true } : {}),
|
|
220
|
+
}),
|
|
221
|
+
}).catch((err) => {
|
|
222
|
+
logger.warn(formatCompact({
|
|
223
|
+
op: 'slack_gateway_receive_failed',
|
|
224
|
+
target: msg.channel,
|
|
225
|
+
error: err instanceof Error ? err.message : String(err),
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
129
228
|
}
|
|
130
229
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.logger.debug(
|
|
140
|
-
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`,
|
|
141
|
-
);
|
|
142
|
-
this.trackMessageChannel(result.ts, options.id);
|
|
143
|
-
return formatSlackMessageRef(options.id, result.ts);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
this.logger.error('Failed to send Slack message:', error);
|
|
146
|
-
throw error;
|
|
230
|
+
admitInteraction(payload: SlackInteractionPayload): void {
|
|
231
|
+
if (!this.#open) return;
|
|
232
|
+
if (payload.type !== 'block_actions' || !payload.actions?.length) return;
|
|
233
|
+
const channelId = payload.channel?.id ?? '';
|
|
234
|
+
const userId = payload.user.id;
|
|
235
|
+
const messageTs = payload.message?.ts ?? '';
|
|
236
|
+
if (payload.response_url) {
|
|
237
|
+
postSlackEphemeral(payload.response_url, '已收到', logger);
|
|
147
238
|
}
|
|
239
|
+
void this.#options.gateway.receive({
|
|
240
|
+
adapter: this.#options.id,
|
|
241
|
+
target: channelId || userId,
|
|
242
|
+
content: formatInteractionContent(payload),
|
|
243
|
+
sender: userId,
|
|
244
|
+
id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
|
|
245
|
+
metadata: Object.freeze({
|
|
246
|
+
endpoint: this.#options.config.name,
|
|
247
|
+
eventType: 'block_actions',
|
|
248
|
+
actionId: payload.actions[0]?.action_id,
|
|
249
|
+
threadTs: messageTs || undefined,
|
|
250
|
+
}),
|
|
251
|
+
}).catch((err) => {
|
|
252
|
+
logger.warn(formatCompact({
|
|
253
|
+
op: 'slack_gateway_receive_failed',
|
|
254
|
+
target: channelId,
|
|
255
|
+
error: err instanceof Error ? err.message : String(err),
|
|
256
|
+
}));
|
|
257
|
+
});
|
|
148
258
|
}
|
|
149
259
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
260
|
+
admitSlashCommand(cmd: SlackSlashCommand): void {
|
|
261
|
+
if (!this.#open) return;
|
|
262
|
+
postSlackEphemeral(cmd.response_url, '处理中…', logger);
|
|
263
|
+
void this.#options.gateway.receive({
|
|
264
|
+
adapter: this.#options.id,
|
|
265
|
+
target: cmd.channel_id,
|
|
266
|
+
content: formatSlashContent(cmd),
|
|
267
|
+
sender: cmd.user_id,
|
|
268
|
+
id: cmd.trigger_id,
|
|
269
|
+
metadata: Object.freeze({
|
|
270
|
+
endpoint: this.#options.config.name,
|
|
271
|
+
eventType: 'slash_command',
|
|
272
|
+
command: cmd.command,
|
|
273
|
+
}),
|
|
274
|
+
}).catch((err) => {
|
|
275
|
+
logger.warn(formatCompact({
|
|
276
|
+
op: 'slack_gateway_receive_failed',
|
|
277
|
+
target: cmd.channel_id,
|
|
278
|
+
error: err instanceof Error ? err.message : String(err),
|
|
279
|
+
}));
|
|
280
|
+
});
|
|
157
281
|
}
|
|
158
282
|
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
this.
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
await this.client!.chat.delete({ channel: ref.channel, ts: ref.ts });
|
|
167
|
-
} catch (error: unknown) {
|
|
168
|
-
const slackError = (error as { data?: { error?: string } })?.data?.error;
|
|
169
|
-
if (slackError === 'message_not_found') {
|
|
170
|
-
this.logger.debug(formatCompact({ op: 'recall_skip', reason: 'message_not_found', message_id: id }));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
throw error;
|
|
283
|
+
handleEnvelope(body: unknown): void {
|
|
284
|
+
const envelope = body as SlackEventEnvelope;
|
|
285
|
+
if (envelope?.type === 'event_callback' && envelope.event) {
|
|
286
|
+
this.admit(envelope.event);
|
|
174
287
|
}
|
|
175
288
|
}
|
|
176
289
|
|
|
177
290
|
trackMessageChannel(ts: string, channel: string): void {
|
|
178
|
-
if (ts && channel)
|
|
179
|
-
this.messageChannelMap.set(ts, channel);
|
|
180
|
-
}
|
|
291
|
+
if (ts && channel) this.#messageChannelMap.set(ts, channel);
|
|
181
292
|
}
|
|
182
293
|
|
|
183
294
|
resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
|
|
@@ -186,155 +297,134 @@ export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessage
|
|
|
186
297
|
this.trackMessageChannel(parsed.ts, parsed.channel);
|
|
187
298
|
return parsed;
|
|
188
299
|
}
|
|
189
|
-
if (channelHint) {
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
const channel = this.messageChannelMap.get(messageId);
|
|
193
|
-
if (channel) {
|
|
194
|
-
return { channel, ts: messageId };
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Activity Feedback:在用户消息上添加表情回应
|
|
201
|
-
* @returns reaction name,供 $removeReaction 使用
|
|
202
|
-
*/
|
|
203
|
-
async $addReaction(
|
|
204
|
-
messageId: string,
|
|
205
|
-
emoji: string,
|
|
206
|
-
hint?: { sceneType?: 'private' | 'group' | 'channel'; channelId?: string },
|
|
207
|
-
): Promise<string | null> {
|
|
208
|
-
const ref = this.resolveMessageRef(messageId, hint?.channelId);
|
|
209
|
-
if (!ref) {
|
|
210
|
-
this.logger.warn(formatCompact({ op: 'reaction_add_skip', reason: 'channel_unknown', message_id: messageId }));
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
const name = normalizeSlackReactionName(emoji);
|
|
214
|
-
try {
|
|
215
|
-
await this.addReaction(ref.channel, ref.ts, name);
|
|
216
|
-
return name;
|
|
217
|
-
} catch (error) {
|
|
218
|
-
this.logger.error('Failed to add Slack reaction:', error);
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
300
|
+
if (channelHint) return { channel: channelHint, ts: messageId };
|
|
301
|
+
const channel = this.#messageChannelMap.get(messageId);
|
|
302
|
+
return channel ? { channel, ts: messageId } : null;
|
|
221
303
|
}
|
|
222
304
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!ref) {
|
|
227
|
-
this.logger.warn(formatCompact({ op: 'reaction_remove_skip', reason: 'channel_unknown', message_id: messageId }));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const name = normalizeSlackReactionName(reactionId);
|
|
231
|
-
try {
|
|
232
|
-
await this.removeReaction(ref.channel, ref.ts, name);
|
|
233
|
-
} catch (error) {
|
|
234
|
-
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
235
|
-
if (code === 'no_reaction') return;
|
|
236
|
-
this.logger.error('Failed to remove Slack reaction:', error);
|
|
237
|
-
}
|
|
305
|
+
async editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
|
|
306
|
+
if (!this.#client) throw new Error('Slack client not connected');
|
|
307
|
+
await editSlackContent(this.#client, channel, messageTs, content);
|
|
238
308
|
}
|
|
239
309
|
|
|
240
|
-
//
|
|
310
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
241
311
|
|
|
242
312
|
async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
|
|
243
|
-
await this
|
|
244
|
-
this.logger.debug(formatCompact({ op: 'invite', endpoint: this.$id, channel, users: users.join(',') }));
|
|
313
|
+
await this.#client!.conversations.invite({ channel, users: users.join(',') });
|
|
245
314
|
return true;
|
|
246
315
|
}
|
|
247
316
|
|
|
248
317
|
async kickFromChannel(channel: string, user: string): Promise<boolean> {
|
|
249
|
-
await this
|
|
250
|
-
this.logger.debug(formatCompact({ op: 'kick', endpoint: this.$id, channel, user }));
|
|
318
|
+
await this.#client!.conversations.kick({ channel, user });
|
|
251
319
|
return true;
|
|
252
320
|
}
|
|
253
321
|
|
|
254
322
|
async setChannelTopic(channel: string, topic: string): Promise<boolean> {
|
|
255
|
-
await this
|
|
256
|
-
this.logger.debug(formatCompact({ op: 'set_topic', endpoint: this.$id, channel }));
|
|
323
|
+
await this.#client!.conversations.setTopic({ channel, topic });
|
|
257
324
|
return true;
|
|
258
325
|
}
|
|
259
326
|
|
|
260
327
|
async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
|
|
261
|
-
await this
|
|
262
|
-
this.logger.debug(formatCompact({ op: 'set_purpose', endpoint: this.$id, channel }));
|
|
328
|
+
await this.#client!.conversations.setPurpose({ channel, purpose });
|
|
263
329
|
return true;
|
|
264
330
|
}
|
|
265
331
|
|
|
266
332
|
async archiveChannel(channel: string): Promise<boolean> {
|
|
267
|
-
await this
|
|
268
|
-
this.logger.debug(formatCompact({ op: 'archive', endpoint: this.$id, channel }));
|
|
333
|
+
await this.#client!.conversations.archive({ channel });
|
|
269
334
|
return true;
|
|
270
335
|
}
|
|
271
336
|
|
|
272
337
|
async unarchiveChannel(channel: string): Promise<boolean> {
|
|
273
|
-
await this
|
|
274
|
-
this.logger.debug(formatCompact({ op: 'unarchive', endpoint: this.$id, channel }));
|
|
338
|
+
await this.#client!.conversations.unarchive({ channel });
|
|
275
339
|
return true;
|
|
276
340
|
}
|
|
277
341
|
|
|
278
342
|
async renameChannel(channel: string, name: string): Promise<boolean> {
|
|
279
|
-
await this
|
|
280
|
-
this.logger.debug(formatCompact({ op: 'rename', endpoint: this.$id, channel, name }));
|
|
343
|
+
await this.#client!.conversations.rename({ channel, name });
|
|
281
344
|
return true;
|
|
282
345
|
}
|
|
283
346
|
|
|
284
347
|
async getChannelMembers(channel: string): Promise<string[]> {
|
|
285
|
-
const result = await this
|
|
348
|
+
const result = await this.#client!.conversations.members({ channel });
|
|
286
349
|
return result.members || [];
|
|
287
350
|
}
|
|
288
351
|
|
|
289
|
-
async getChannelInfo(channel: string): Promise<
|
|
290
|
-
const result = await this
|
|
352
|
+
async getChannelInfo(channel: string): Promise<unknown> {
|
|
353
|
+
const result = await this.#client!.conversations.info({ channel });
|
|
291
354
|
return result.channel;
|
|
292
355
|
}
|
|
293
356
|
|
|
294
|
-
async getUserInfo(user: string): Promise<
|
|
295
|
-
const result = await this
|
|
357
|
+
async getUserInfo(user: string): Promise<unknown> {
|
|
358
|
+
const result = await this.#client!.users.info({ user });
|
|
296
359
|
return result.user;
|
|
297
360
|
}
|
|
298
361
|
|
|
299
362
|
async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
363
|
+
const reaction = normalizeSlackReactionName(name);
|
|
300
364
|
try {
|
|
301
|
-
await this
|
|
302
|
-
this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name }));
|
|
365
|
+
await this.#client!.reactions.add({ channel, timestamp, name: reaction });
|
|
303
366
|
return true;
|
|
304
367
|
} catch (error) {
|
|
305
368
|
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
306
|
-
if (code === 'already_reacted')
|
|
307
|
-
this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name, status: 'exists' }));
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
369
|
+
if (code === 'already_reacted') return true;
|
|
310
370
|
throw error;
|
|
311
371
|
}
|
|
312
372
|
}
|
|
313
373
|
|
|
314
374
|
async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
375
|
+
const reaction = normalizeSlackReactionName(name);
|
|
315
376
|
try {
|
|
316
|
-
await this
|
|
317
|
-
this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name }));
|
|
377
|
+
await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
|
|
318
378
|
return true;
|
|
319
379
|
} catch (error) {
|
|
320
380
|
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
321
|
-
if (code === 'no_reaction')
|
|
322
|
-
this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name, status: 'gone' }));
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
381
|
+
if (code === 'no_reaction') return true;
|
|
325
382
|
throw error;
|
|
326
383
|
}
|
|
327
384
|
}
|
|
328
385
|
|
|
329
386
|
async pinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
330
|
-
await this
|
|
331
|
-
this.logger.debug(formatCompact({ op: 'pin', endpoint: this.$id, channel }));
|
|
387
|
+
await this.#client!.pins.add({ channel, timestamp });
|
|
332
388
|
return true;
|
|
333
389
|
}
|
|
334
390
|
|
|
335
391
|
async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
336
|
-
await this
|
|
337
|
-
this.logger.debug(formatCompact({ op: 'unpin', endpoint: this.$id, channel }));
|
|
392
|
+
await this.#client!.pins.remove({ channel, timestamp });
|
|
338
393
|
return true;
|
|
339
394
|
}
|
|
395
|
+
|
|
396
|
+
async #startSocket(): Promise<void> {
|
|
397
|
+
const { config } = this.#options;
|
|
398
|
+
if (!config.appToken) throw new Error('Socket Mode requires appToken');
|
|
399
|
+
this.#socket = this.#options.createSocket?.({
|
|
400
|
+
appToken: config.appToken,
|
|
401
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
402
|
+
}) ?? new SocketModeClient({
|
|
403
|
+
appToken: config.appToken,
|
|
404
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
405
|
+
}) as unknown as SlackSocketLike;
|
|
406
|
+
|
|
407
|
+
this.#socket.on('slack_event', async ({ ack, body }) => {
|
|
408
|
+
await ack();
|
|
409
|
+
this.handleEnvelope(body);
|
|
410
|
+
});
|
|
411
|
+
this.#socket.on('interactive', async ({ ack, body }) => {
|
|
412
|
+
await ack();
|
|
413
|
+
this.admitInteraction(body as SlackInteractionPayload);
|
|
414
|
+
});
|
|
415
|
+
this.#socket.on('slash_commands', async ({ ack, body }) => {
|
|
416
|
+
await ack();
|
|
417
|
+
this.admitSlashCommand(body as SlackSlashCommand);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await this.#socket.start();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parseSendTarget(target: string): { channel: string; threadTs?: string } {
|
|
425
|
+
const parsed = parseSlackMessageRef(target);
|
|
426
|
+
if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
|
|
427
|
+
return { channel: parsed.channel, threadTs: parsed.ts };
|
|
428
|
+
}
|
|
429
|
+
return { channel: target };
|
|
340
430
|
}
|