@zhin.js/adapter-slack 4.1.0 → 5.0.0
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 +71 -0
- package/README.md +76 -252
- 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 +149 -0
- package/lib/endpoint.js +339 -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 +59 -0
- package/src/endpoint.ts +322 -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,299 @@
|
|
|
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
|
-
|
|
31
|
-
get
|
|
105
|
+
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
106
|
+
get name(): string {
|
|
107
|
+
return this.#options.config.name;
|
|
108
|
+
}
|
|
32
109
|
|
|
33
|
-
|
|
34
|
-
this
|
|
110
|
+
get client(): SlackWebClientLike | undefined {
|
|
111
|
+
return this.#client;
|
|
35
112
|
}
|
|
36
113
|
|
|
37
|
-
|
|
114
|
+
get platformUserId(): string | undefined {
|
|
115
|
+
return this.#botUserId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get config(): ResolvedSlackConfig {
|
|
119
|
+
return this.#options.config;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async start(): Promise<void> {
|
|
123
|
+
if (this.#started) return;
|
|
124
|
+
this.#started = true;
|
|
38
125
|
try {
|
|
39
|
-
|
|
40
|
-
this
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this
|
|
47
|
-
|
|
126
|
+
const { config } = this.#options;
|
|
127
|
+
this.#client = this.#options.createClient?.(config.token)
|
|
128
|
+
?? (new WebClient(config.token) as unknown as SlackWebClientLike);
|
|
129
|
+
|
|
130
|
+
this.#unregisterAgent = registerSlackAgentEndpoint(config.name, this);
|
|
131
|
+
|
|
132
|
+
if (config.mode === 'socket') {
|
|
133
|
+
await this.#startSocket();
|
|
134
|
+
} else {
|
|
135
|
+
const http = this.#options.http;
|
|
136
|
+
if (!http) {
|
|
137
|
+
throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
|
|
138
|
+
}
|
|
139
|
+
this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
|
|
140
|
+
logger.debug(formatCompact({
|
|
141
|
+
endpoint: config.name,
|
|
142
|
+
op: 'webhook',
|
|
143
|
+
path: config.webhookPath,
|
|
144
|
+
}));
|
|
48
145
|
}
|
|
49
146
|
|
|
50
|
-
|
|
147
|
+
const authTest = await this.#client.auth.test();
|
|
148
|
+
if (authTest.user_id) this.#botUserId = String(authTest.user_id);
|
|
51
149
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
endpoint: this.$config.name,
|
|
58
|
-
user: authTest.user,
|
|
59
|
-
platform_user_id: this.$platformUserId,
|
|
150
|
+
logger.info(formatCompact({
|
|
151
|
+
op: 'connect',
|
|
152
|
+
endpoint: config.name,
|
|
153
|
+
mode: config.mode,
|
|
154
|
+
platform_user_id: this.#botUserId,
|
|
60
155
|
}));
|
|
61
|
-
|
|
62
|
-
if (!this.$config.socketMode && router) {
|
|
63
|
-
this.logger.debug(formatCompact({ op: 'listen', mode: 'http', path: '/slack/events' }));
|
|
64
|
-
}
|
|
65
156
|
} catch (error) {
|
|
66
|
-
this.
|
|
67
|
-
|
|
157
|
+
await this.stop();
|
|
158
|
+
logger.error('Failed to connect Slack endpoint:', error);
|
|
68
159
|
throw error;
|
|
69
160
|
}
|
|
70
161
|
}
|
|
71
162
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
163
|
+
open(): void {
|
|
164
|
+
this.#open = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
close(): void {
|
|
168
|
+
this.#open = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async stop(): Promise<void> {
|
|
172
|
+
this.#open = false;
|
|
173
|
+
if (this.#socket) {
|
|
174
|
+
try {
|
|
175
|
+
await this.#socket.disconnect();
|
|
176
|
+
} catch {
|
|
177
|
+
/* ignore */
|
|
77
178
|
}
|
|
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;
|
|
179
|
+
this.#socket = undefined;
|
|
84
180
|
}
|
|
181
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
182
|
+
this.#unregisterAgent?.();
|
|
183
|
+
this.#unregisterAgent = undefined;
|
|
184
|
+
this.#client = undefined;
|
|
185
|
+
this.#started = false;
|
|
186
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
85
187
|
}
|
|
86
188
|
|
|
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
|
-
});
|
|
189
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
190
|
+
if (!this.#client) throw new Error('Slack client not connected');
|
|
191
|
+
const { channel, threadTs } = parseSendTarget(target);
|
|
192
|
+
const result = await sendSlackContent(
|
|
193
|
+
this.#client,
|
|
194
|
+
payload,
|
|
195
|
+
{ channel, threadTs },
|
|
196
|
+
logger,
|
|
197
|
+
);
|
|
198
|
+
if (result.ts) this.trackMessageChannel(result.ts, channel);
|
|
199
|
+
return formatSlackMessageRef(channel, result.ts || String(Date.now()));
|
|
200
|
+
}
|
|
127
201
|
|
|
128
|
-
|
|
202
|
+
/** Test / internal: admit a message event when open. */
|
|
203
|
+
admit(event: SlackMessageEvent | SlackEvent): void {
|
|
204
|
+
if (!this.#open) return;
|
|
205
|
+
if (event.type !== 'message' && event.type !== 'app_mention') return;
|
|
206
|
+
const msg = event as SlackMessageEvent;
|
|
207
|
+
if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId)) return;
|
|
208
|
+
if (!msg.channel || !msg.ts) return;
|
|
209
|
+
|
|
210
|
+
this.trackMessageChannel(msg.ts, msg.channel);
|
|
211
|
+
void this.#options.gateway.receive({
|
|
212
|
+
adapter: this.#options.id,
|
|
213
|
+
target: msg.channel,
|
|
214
|
+
content: formatInboundContent(msg),
|
|
215
|
+
sender: msg.user ?? msg.channel,
|
|
216
|
+
id: inboundMessageId(msg),
|
|
217
|
+
metadata: Object.freeze({
|
|
218
|
+
endpoint: this.#options.config.name,
|
|
219
|
+
channelType: resolveSlackChannelType(msg),
|
|
220
|
+
userId: msg.user,
|
|
221
|
+
threadTs: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
|
|
222
|
+
ts: msg.ts,
|
|
223
|
+
// app_mention 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
224
|
+
...(msg.type === 'app_mention' ? { mentioned: true } : {}),
|
|
225
|
+
}),
|
|
226
|
+
}).catch((err) => {
|
|
227
|
+
logger.warn(formatCompact({
|
|
228
|
+
op: 'slack_gateway_receive_failed',
|
|
229
|
+
target: msg.channel,
|
|
230
|
+
error: err instanceof Error ? err.message : String(err),
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
129
233
|
}
|
|
130
234
|
|
|
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;
|
|
235
|
+
admitInteraction(payload: SlackInteractionPayload): void {
|
|
236
|
+
if (!this.#open) return;
|
|
237
|
+
if (payload.type !== 'block_actions' || !payload.actions?.length) return;
|
|
238
|
+
const channelId = payload.channel?.id ?? '';
|
|
239
|
+
const userId = payload.user.id;
|
|
240
|
+
const messageTs = payload.message?.ts ?? '';
|
|
241
|
+
if (payload.response_url) {
|
|
242
|
+
postSlackEphemeral(payload.response_url, '已收到', logger);
|
|
147
243
|
}
|
|
244
|
+
void this.#options.gateway.receive({
|
|
245
|
+
adapter: this.#options.id,
|
|
246
|
+
target: channelId || userId,
|
|
247
|
+
content: formatInteractionContent(payload),
|
|
248
|
+
sender: userId,
|
|
249
|
+
id: payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`,
|
|
250
|
+
metadata: Object.freeze({
|
|
251
|
+
endpoint: this.#options.config.name,
|
|
252
|
+
eventType: 'block_actions',
|
|
253
|
+
actionId: payload.actions[0]?.action_id,
|
|
254
|
+
threadTs: messageTs || undefined,
|
|
255
|
+
}),
|
|
256
|
+
}).catch((err) => {
|
|
257
|
+
logger.warn(formatCompact({
|
|
258
|
+
op: 'slack_gateway_receive_failed',
|
|
259
|
+
target: channelId,
|
|
260
|
+
error: err instanceof Error ? err.message : String(err),
|
|
261
|
+
}));
|
|
262
|
+
});
|
|
148
263
|
}
|
|
149
264
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
265
|
+
admitSlashCommand(cmd: SlackSlashCommand): void {
|
|
266
|
+
if (!this.#open) return;
|
|
267
|
+
postSlackEphemeral(cmd.response_url, '处理中…', logger);
|
|
268
|
+
void this.#options.gateway.receive({
|
|
269
|
+
adapter: this.#options.id,
|
|
270
|
+
target: cmd.channel_id,
|
|
271
|
+
content: formatSlashContent(cmd),
|
|
272
|
+
sender: cmd.user_id,
|
|
273
|
+
id: cmd.trigger_id,
|
|
274
|
+
metadata: Object.freeze({
|
|
275
|
+
endpoint: this.#options.config.name,
|
|
276
|
+
eventType: 'slash_command',
|
|
277
|
+
command: cmd.command,
|
|
278
|
+
}),
|
|
279
|
+
}).catch((err) => {
|
|
280
|
+
logger.warn(formatCompact({
|
|
281
|
+
op: 'slack_gateway_receive_failed',
|
|
282
|
+
target: cmd.channel_id,
|
|
283
|
+
error: err instanceof Error ? err.message : String(err),
|
|
284
|
+
}));
|
|
285
|
+
});
|
|
157
286
|
}
|
|
158
287
|
|
|
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;
|
|
288
|
+
handleEnvelope(body: unknown): void {
|
|
289
|
+
const envelope = body as SlackEventEnvelope;
|
|
290
|
+
if (envelope?.type === 'event_callback' && envelope.event) {
|
|
291
|
+
this.admit(envelope.event);
|
|
174
292
|
}
|
|
175
293
|
}
|
|
176
294
|
|
|
177
295
|
trackMessageChannel(ts: string, channel: string): void {
|
|
178
|
-
if (ts && channel)
|
|
179
|
-
this.messageChannelMap.set(ts, channel);
|
|
180
|
-
}
|
|
296
|
+
if (ts && channel) this.#messageChannelMap.set(ts, channel);
|
|
181
297
|
}
|
|
182
298
|
|
|
183
299
|
resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
|
|
@@ -186,155 +302,134 @@ export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessage
|
|
|
186
302
|
this.trackMessageChannel(parsed.ts, parsed.channel);
|
|
187
303
|
return parsed;
|
|
188
304
|
}
|
|
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
|
-
}
|
|
305
|
+
if (channelHint) return { channel: channelHint, ts: messageId };
|
|
306
|
+
const channel = this.#messageChannelMap.get(messageId);
|
|
307
|
+
return channel ? { channel, ts: messageId } : null;
|
|
221
308
|
}
|
|
222
309
|
|
|
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
|
-
}
|
|
310
|
+
async editMessage(channel: string, messageTs: string, content: unknown): Promise<void> {
|
|
311
|
+
if (!this.#client) throw new Error('Slack client not connected');
|
|
312
|
+
await editSlackContent(this.#client, channel, messageTs, content);
|
|
238
313
|
}
|
|
239
314
|
|
|
240
|
-
//
|
|
315
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
241
316
|
|
|
242
317
|
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(',') }));
|
|
318
|
+
await this.#client!.conversations.invite({ channel, users: users.join(',') });
|
|
245
319
|
return true;
|
|
246
320
|
}
|
|
247
321
|
|
|
248
322
|
async kickFromChannel(channel: string, user: string): Promise<boolean> {
|
|
249
|
-
await this
|
|
250
|
-
this.logger.debug(formatCompact({ op: 'kick', endpoint: this.$id, channel, user }));
|
|
323
|
+
await this.#client!.conversations.kick({ channel, user });
|
|
251
324
|
return true;
|
|
252
325
|
}
|
|
253
326
|
|
|
254
327
|
async setChannelTopic(channel: string, topic: string): Promise<boolean> {
|
|
255
|
-
await this
|
|
256
|
-
this.logger.debug(formatCompact({ op: 'set_topic', endpoint: this.$id, channel }));
|
|
328
|
+
await this.#client!.conversations.setTopic({ channel, topic });
|
|
257
329
|
return true;
|
|
258
330
|
}
|
|
259
331
|
|
|
260
332
|
async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
|
|
261
|
-
await this
|
|
262
|
-
this.logger.debug(formatCompact({ op: 'set_purpose', endpoint: this.$id, channel }));
|
|
333
|
+
await this.#client!.conversations.setPurpose({ channel, purpose });
|
|
263
334
|
return true;
|
|
264
335
|
}
|
|
265
336
|
|
|
266
337
|
async archiveChannel(channel: string): Promise<boolean> {
|
|
267
|
-
await this
|
|
268
|
-
this.logger.debug(formatCompact({ op: 'archive', endpoint: this.$id, channel }));
|
|
338
|
+
await this.#client!.conversations.archive({ channel });
|
|
269
339
|
return true;
|
|
270
340
|
}
|
|
271
341
|
|
|
272
342
|
async unarchiveChannel(channel: string): Promise<boolean> {
|
|
273
|
-
await this
|
|
274
|
-
this.logger.debug(formatCompact({ op: 'unarchive', endpoint: this.$id, channel }));
|
|
343
|
+
await this.#client!.conversations.unarchive({ channel });
|
|
275
344
|
return true;
|
|
276
345
|
}
|
|
277
346
|
|
|
278
347
|
async renameChannel(channel: string, name: string): Promise<boolean> {
|
|
279
|
-
await this
|
|
280
|
-
this.logger.debug(formatCompact({ op: 'rename', endpoint: this.$id, channel, name }));
|
|
348
|
+
await this.#client!.conversations.rename({ channel, name });
|
|
281
349
|
return true;
|
|
282
350
|
}
|
|
283
351
|
|
|
284
352
|
async getChannelMembers(channel: string): Promise<string[]> {
|
|
285
|
-
const result = await this
|
|
353
|
+
const result = await this.#client!.conversations.members({ channel });
|
|
286
354
|
return result.members || [];
|
|
287
355
|
}
|
|
288
356
|
|
|
289
|
-
async getChannelInfo(channel: string): Promise<
|
|
290
|
-
const result = await this
|
|
357
|
+
async getChannelInfo(channel: string): Promise<unknown> {
|
|
358
|
+
const result = await this.#client!.conversations.info({ channel });
|
|
291
359
|
return result.channel;
|
|
292
360
|
}
|
|
293
361
|
|
|
294
|
-
async getUserInfo(user: string): Promise<
|
|
295
|
-
const result = await this
|
|
362
|
+
async getUserInfo(user: string): Promise<unknown> {
|
|
363
|
+
const result = await this.#client!.users.info({ user });
|
|
296
364
|
return result.user;
|
|
297
365
|
}
|
|
298
366
|
|
|
299
367
|
async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
368
|
+
const reaction = normalizeSlackReactionName(name);
|
|
300
369
|
try {
|
|
301
|
-
await this
|
|
302
|
-
this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name }));
|
|
370
|
+
await this.#client!.reactions.add({ channel, timestamp, name: reaction });
|
|
303
371
|
return true;
|
|
304
372
|
} catch (error) {
|
|
305
373
|
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
|
-
}
|
|
374
|
+
if (code === 'already_reacted') return true;
|
|
310
375
|
throw error;
|
|
311
376
|
}
|
|
312
377
|
}
|
|
313
378
|
|
|
314
379
|
async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
380
|
+
const reaction = normalizeSlackReactionName(name);
|
|
315
381
|
try {
|
|
316
|
-
await this
|
|
317
|
-
this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name }));
|
|
382
|
+
await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
|
|
318
383
|
return true;
|
|
319
384
|
} catch (error) {
|
|
320
385
|
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
|
-
}
|
|
386
|
+
if (code === 'no_reaction') return true;
|
|
325
387
|
throw error;
|
|
326
388
|
}
|
|
327
389
|
}
|
|
328
390
|
|
|
329
391
|
async pinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
330
|
-
await this
|
|
331
|
-
this.logger.debug(formatCompact({ op: 'pin', endpoint: this.$id, channel }));
|
|
392
|
+
await this.#client!.pins.add({ channel, timestamp });
|
|
332
393
|
return true;
|
|
333
394
|
}
|
|
334
395
|
|
|
335
396
|
async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
336
|
-
await this
|
|
337
|
-
this.logger.debug(formatCompact({ op: 'unpin', endpoint: this.$id, channel }));
|
|
397
|
+
await this.#client!.pins.remove({ channel, timestamp });
|
|
338
398
|
return true;
|
|
339
399
|
}
|
|
400
|
+
|
|
401
|
+
async #startSocket(): Promise<void> {
|
|
402
|
+
const { config } = this.#options;
|
|
403
|
+
if (!config.appToken) throw new Error('Socket Mode requires appToken');
|
|
404
|
+
this.#socket = this.#options.createSocket?.({
|
|
405
|
+
appToken: config.appToken,
|
|
406
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
407
|
+
}) ?? new SocketModeClient({
|
|
408
|
+
appToken: config.appToken,
|
|
409
|
+
clientPingTimeout: config.clientPingTimeout,
|
|
410
|
+
}) as unknown as SlackSocketLike;
|
|
411
|
+
|
|
412
|
+
this.#socket.on('slack_event', async ({ ack, body }) => {
|
|
413
|
+
await ack();
|
|
414
|
+
this.handleEnvelope(body);
|
|
415
|
+
});
|
|
416
|
+
this.#socket.on('interactive', async ({ ack, body }) => {
|
|
417
|
+
await ack();
|
|
418
|
+
this.admitInteraction(body as SlackInteractionPayload);
|
|
419
|
+
});
|
|
420
|
+
this.#socket.on('slash_commands', async ({ ack, body }) => {
|
|
421
|
+
await ack();
|
|
422
|
+
this.admitSlashCommand(body as SlackSlashCommand);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
await this.#socket.start();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseSendTarget(target: string): { channel: string; threadTs?: string } {
|
|
430
|
+
const parsed = parseSlackMessageRef(target);
|
|
431
|
+
if (parsed && /^\d+\.\d+$/.test(parsed.ts)) {
|
|
432
|
+
return { channel: parsed.channel, threadTs: parsed.ts };
|
|
433
|
+
}
|
|
434
|
+
return { channel: target };
|
|
340
435
|
}
|