@zhin.js/adapter-slack 4.0.1 → 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 +59 -0
- package/README.md +77 -160
- package/adapters/slack.ts +34 -0
- package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
- package/agent/tools/add_reaction.ts +26 -0
- package/agent/tools/archive_channel.ts +24 -0
- package/agent/tools/edit_message.ts +28 -0
- package/agent/tools/invite_to_channel.ts +26 -0
- package/agent/tools/pin_message.ts +26 -0
- package/agent/tools/remove_reaction.ts +26 -0
- package/agent/tools/set_purpose.ts +26 -0
- package/agent/tools/set_topic.ts +26 -0
- package/agent/tools/unarchive.ts +24 -0
- package/agent/tools/unpin_message.ts +26 -0
- package/agent/tools/user_info.ts +31 -0
- package/lib/endpoint.d.ts +133 -94
- package/lib/endpoint.js +270 -612
- package/lib/index.d.ts +11 -10
- package/lib/index.js +11 -252
- package/lib/markdown-to-mrkdwn.d.ts +9 -0
- package/lib/markdown-to-mrkdwn.js +121 -0
- package/lib/mrkdwn-to-markdown.d.ts +4 -0
- package/lib/mrkdwn-to-markdown.js +30 -0
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -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/slack-inbound-filter.js +46 -0
- package/lib/slack-message-ref.d.ts +7 -0
- package/lib/slack-message-ref.js +17 -0
- package/lib/slack-outbound.d.ts +24 -0
- package/lib/slack-outbound.js +109 -0
- package/lib/slack-reaction.d.ts +1 -0
- package/lib/slack-reaction.js +26 -0
- package/lib/slack-response-url.d.ts +5 -0
- package/lib/slack-response-url.js +16 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +69 -0
- package/package.json +53 -14
- package/plugin.ts +13 -0
- package/schema.json +38 -0
- package/src/endpoint.ts +339 -647
- package/src/index.ts +65 -277
- package/src/markdown-to-mrkdwn.ts +117 -0
- package/src/mrkdwn-to-markdown.ts +29 -0
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +380 -0
- package/src/slack-agent-deps.ts +57 -0
- package/src/slack-inbound-filter.ts +60 -0
- package/src/slack-message-ref.ts +18 -0
- package/src/slack-outbound.ts +167 -0
- package/src/slack-reaction.ts +23 -0
- package/src/slack-response-url.ts +26 -0
- package/src/webhook.ts +103 -0
- package/lib/adapter.d.ts +0 -19
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -46
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -17
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -54
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -18
- /package/{skills/slack → agent}/PERMITS.md +0 -0
package/src/endpoint.ts
CHANGED
|
@@ -1,738 +1,430 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { WebClient
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
4
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
5
|
+
import { WebClient } from '@slack/web-api';
|
|
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';
|
|
29
|
+
import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
|
|
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
|
+
}
|
|
76
|
+
|
|
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
|
+
}
|
|
88
|
+
|
|
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;
|
|
51
103
|
}
|
|
52
104
|
|
|
53
|
-
|
|
54
|
-
this.#
|
|
105
|
+
get client(): SlackWebClientLike | undefined {
|
|
106
|
+
return this.#client;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get platformUserId(): string | undefined {
|
|
110
|
+
return this.#botUserId;
|
|
111
|
+
}
|
|
112
|
+
|
|
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;
|
|
55
120
|
try {
|
|
56
|
-
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await this
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Start the app
|
|
67
|
-
const port = this.$config.port || 3000;
|
|
68
|
-
if (this.$config.socketMode) {
|
|
69
|
-
await this.app!.start();
|
|
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();
|
|
70
129
|
} else {
|
|
71
|
-
|
|
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
|
+
}));
|
|
72
140
|
}
|
|
73
141
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Get bot info
|
|
77
|
-
const authTest = await this.client!.auth.test();
|
|
78
|
-
this.logger.info(formatCompact({ endpoint: this.$config.name, user: authTest.user }));
|
|
142
|
+
const authTest = await this.#client.auth.test();
|
|
143
|
+
if (authTest.user_id) this.#botUserId = String(authTest.user_id);
|
|
79
144
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
145
|
+
logger.info(formatCompact({
|
|
146
|
+
op: 'connect',
|
|
147
|
+
endpoint: config.name,
|
|
148
|
+
mode: config.mode,
|
|
149
|
+
platform_user_id: this.#botUserId,
|
|
150
|
+
}));
|
|
83
151
|
} catch (error) {
|
|
84
|
-
this.
|
|
85
|
-
|
|
152
|
+
await this.stop();
|
|
153
|
+
logger.error('Failed to connect Slack endpoint:', error);
|
|
86
154
|
throw error;
|
|
87
155
|
}
|
|
88
156
|
}
|
|
89
157
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.$connected = false;
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
await this.app!.stop();
|
|
97
|
-
this.$connected = false;
|
|
98
|
-
this.logger.info(formatCompact( { op: "disconnect", endpoint: this.$config.name }));
|
|
99
|
-
} catch (error) {
|
|
100
|
-
this.logger.error("Error disconnecting Slack bot:", error);
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
158
|
+
open(): void {
|
|
159
|
+
this.#open = true;
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
const channelId = msg.channel;
|
|
113
|
-
const userId = msg.user;
|
|
114
|
-
const key = `${channelId}:${userId}`;
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
const cached = this.senderPermitCache.get(key);
|
|
117
|
-
if (cached && now - cached.at < 60_000) {
|
|
118
|
-
message.$sender.role = cached.role;
|
|
119
|
-
message.$sender.permissions = cached.permissions;
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
const user = await this.getUserInfo(userId);
|
|
124
|
-
let isChannelManager = false;
|
|
162
|
+
close(): void {
|
|
163
|
+
this.#open = false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async stop(): Promise<void> {
|
|
167
|
+
this.#open = false;
|
|
168
|
+
if (this.#socket) {
|
|
125
169
|
try {
|
|
126
|
-
|
|
127
|
-
if (channel?.creator === userId) isChannelManager = true;
|
|
170
|
+
await this.#socket.disconnect();
|
|
128
171
|
} catch {
|
|
129
|
-
|
|
172
|
+
/* ignore */
|
|
130
173
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
permissions: normalized.permissions ?? [],
|
|
140
|
-
};
|
|
141
|
-
this.senderPermitCache.set(key, entry);
|
|
142
|
-
message.$sender.role = entry.role;
|
|
143
|
-
message.$sender.permissions = entry.permissions;
|
|
144
|
-
} catch {
|
|
145
|
-
// 保守拒绝
|
|
146
|
-
}
|
|
174
|
+
this.#socket = undefined;
|
|
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 }));
|
|
147
182
|
}
|
|
148
183
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.adapter.emit("message.receive", message);
|
|
158
|
-
this.logger.debug(
|
|
159
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`
|
|
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,
|
|
160
192
|
);
|
|
193
|
+
if (result.ts) this.trackMessageChannel(result.ts, channel);
|
|
194
|
+
return formatSlackMessageRef(channel, result.ts || String(Date.now()));
|
|
161
195
|
}
|
|
162
196
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await this.client!.chat.delete({
|
|
195
|
-
channel: channelId,
|
|
196
|
-
ts: result.$id,
|
|
197
|
-
});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
this.logger.error("Error recalling Slack message:", error);
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
$reply: async (
|
|
204
|
-
content: SendContent,
|
|
205
|
-
quote?: boolean | string
|
|
206
|
-
): Promise<string> => {
|
|
207
|
-
if (!Array.isArray(content)) content = [content];
|
|
208
|
-
|
|
209
|
-
const sendOptions: Partial<ChatPostMessageArguments> = {
|
|
210
|
-
channel: channelId,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Handle thread reply
|
|
214
|
-
if (quote) {
|
|
215
|
-
const threadTs = typeof quote === "boolean" ? result.$id : quote;
|
|
216
|
-
sendOptions.thread_ts = threadTs;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return await this.adapter.sendMessage({
|
|
220
|
-
context: "slack",
|
|
221
|
-
endpoint: this.$config.name,
|
|
222
|
-
id: channelId,
|
|
223
|
-
type: "channel",
|
|
224
|
-
content: content,
|
|
225
|
-
});
|
|
226
|
-
},
|
|
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
227
|
});
|
|
228
|
-
|
|
229
|
-
return result;
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
name: file.name,
|
|
261
|
-
url: file.url_private || file.permalink,
|
|
262
|
-
size: file.size,
|
|
263
|
-
mimetype: file.mimetype,
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
} else if (file.mimetype?.startsWith("audio/")) {
|
|
267
|
-
segments.push({
|
|
268
|
-
type: "audio",
|
|
269
|
-
data: {
|
|
270
|
-
id: file.id,
|
|
271
|
-
name: file.name,
|
|
272
|
-
url: file.url_private || file.permalink,
|
|
273
|
-
size: file.size,
|
|
274
|
-
mimetype: file.mimetype,
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
} else {
|
|
278
|
-
segments.push({
|
|
279
|
-
type: "file",
|
|
280
|
-
data: {
|
|
281
|
-
id: file.id,
|
|
282
|
-
name: file.name,
|
|
283
|
-
url: file.url_private || file.permalink,
|
|
284
|
-
size: file.size,
|
|
285
|
-
mimetype: file.mimetype,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Handle attachments
|
|
293
|
-
if ("attachments" in msg && msg.attachments) {
|
|
294
|
-
for (const attachment of msg.attachments) {
|
|
295
|
-
if (attachment.image_url) {
|
|
296
|
-
segments.push({
|
|
297
|
-
type: "image",
|
|
298
|
-
data: {
|
|
299
|
-
url: attachment.image_url,
|
|
300
|
-
title: attachment.title,
|
|
301
|
-
text: attachment.text,
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return segments.length > 0
|
|
309
|
-
? segments
|
|
310
|
-
: [{ type: "text", data: { text: "" } }];
|
|
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);
|
|
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
|
+
});
|
|
311
258
|
}
|
|
312
259
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
while ((match = channelMentionRegex.exec(text)) !== null) {
|
|
335
|
-
allMatches.push({ match, type: "channel" });
|
|
336
|
-
}
|
|
337
|
-
while ((match = linkRegex.exec(text)) !== null) {
|
|
338
|
-
allMatches.push({ match, type: "link" });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Sort by position
|
|
342
|
-
allMatches.sort((a, b) => a.match.index! - b.match.index!);
|
|
343
|
-
|
|
344
|
-
// Process matches
|
|
345
|
-
for (const { match, type } of allMatches) {
|
|
346
|
-
const matchStart = match.index!;
|
|
347
|
-
const matchEnd = matchStart + match[0].length;
|
|
348
|
-
|
|
349
|
-
// Add text before match
|
|
350
|
-
if (matchStart > lastIndex) {
|
|
351
|
-
const beforeText = text.slice(lastIndex, matchStart);
|
|
352
|
-
if (beforeText.trim()) {
|
|
353
|
-
segments.push({ type: "text", data: { text: beforeText } });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Add special segment
|
|
358
|
-
switch (type) {
|
|
359
|
-
case "user":
|
|
360
|
-
segments.push({
|
|
361
|
-
type: "at",
|
|
362
|
-
data: {
|
|
363
|
-
id: match[1],
|
|
364
|
-
name: match[2] || match[1],
|
|
365
|
-
text: match[0],
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
break;
|
|
369
|
-
|
|
370
|
-
case "channel":
|
|
371
|
-
segments.push({
|
|
372
|
-
type: "channel_mention",
|
|
373
|
-
data: {
|
|
374
|
-
id: match[1],
|
|
375
|
-
name: match[2] || match[1],
|
|
376
|
-
text: match[0],
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
break;
|
|
380
|
-
|
|
381
|
-
case "link":
|
|
382
|
-
segments.push({
|
|
383
|
-
type: "link",
|
|
384
|
-
data: {
|
|
385
|
-
url: match[1],
|
|
386
|
-
text: match[2] || match[1],
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
lastIndex = matchEnd;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Add remaining text
|
|
396
|
-
if (lastIndex < text.length) {
|
|
397
|
-
const remainingText = text.slice(lastIndex);
|
|
398
|
-
if (remainingText.trim()) {
|
|
399
|
-
segments.push({ type: "text", data: { text: remainingText } });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return segments.length > 0
|
|
404
|
-
? segments
|
|
405
|
-
: [{ type: "text", data: { text } }];
|
|
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
|
+
});
|
|
406
281
|
}
|
|
407
282
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const result = await this.sendContentToChannel(
|
|
413
|
-
options.id,
|
|
414
|
-
wire
|
|
415
|
-
);
|
|
416
|
-
this.logger.debug(
|
|
417
|
-
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`
|
|
418
|
-
);
|
|
419
|
-
return result.ts || "";
|
|
420
|
-
} catch (error) {
|
|
421
|
-
this.logger.error("Failed to send Slack message:", error);
|
|
422
|
-
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);
|
|
423
287
|
}
|
|
424
288
|
}
|
|
425
289
|
|
|
426
|
-
|
|
427
|
-
channel
|
|
428
|
-
|
|
429
|
-
extraOptions: Partial<ChatPostMessageArguments> = {}
|
|
430
|
-
): Promise<any> {
|
|
431
|
-
if (!Array.isArray(content)) content = [content];
|
|
432
|
-
|
|
433
|
-
let textContent = "";
|
|
434
|
-
const attachments: any[] = [];
|
|
435
|
-
|
|
436
|
-
for (const segment of content) {
|
|
437
|
-
if (typeof segment === "string") {
|
|
438
|
-
textContent += segment;
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const { type, data } = segment;
|
|
443
|
-
|
|
444
|
-
switch (type) {
|
|
445
|
-
case "text":
|
|
446
|
-
textContent += data.text || "";
|
|
447
|
-
break;
|
|
448
|
-
|
|
449
|
-
case "at":
|
|
450
|
-
textContent += `<@${data.id}>`;
|
|
451
|
-
break;
|
|
452
|
-
|
|
453
|
-
case "channel_mention":
|
|
454
|
-
textContent += `<#${data.id}>`;
|
|
455
|
-
break;
|
|
456
|
-
|
|
457
|
-
case "link":
|
|
458
|
-
if (data.text && data.text !== data.url) {
|
|
459
|
-
textContent += `<${data.url}|${data.text}>`;
|
|
460
|
-
} else {
|
|
461
|
-
textContent += `<${data.url}>`;
|
|
462
|
-
}
|
|
463
|
-
break;
|
|
464
|
-
|
|
465
|
-
case "image":
|
|
466
|
-
if (data.url) {
|
|
467
|
-
attachments.push({
|
|
468
|
-
image_url: data.url,
|
|
469
|
-
title: data.name || data.title,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
break;
|
|
473
|
-
|
|
474
|
-
case "file":
|
|
475
|
-
// Files need to be uploaded separately
|
|
476
|
-
if (data.file) {
|
|
477
|
-
try {
|
|
478
|
-
await this.client!.files.upload({
|
|
479
|
-
channels: channel,
|
|
480
|
-
file: data.file,
|
|
481
|
-
filename: data.name,
|
|
482
|
-
});
|
|
483
|
-
} catch (error) {
|
|
484
|
-
this.logger.error("Failed to upload file:", error);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
break;
|
|
488
|
-
|
|
489
|
-
default:
|
|
490
|
-
textContent += data.text || `[${type}]`;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Send message
|
|
495
|
-
const messageOptions: any = {
|
|
496
|
-
channel,
|
|
497
|
-
text: textContent.trim() || "Message",
|
|
498
|
-
...extraOptions,
|
|
499
|
-
};
|
|
290
|
+
trackMessageChannel(ts: string, channel: string): void {
|
|
291
|
+
if (ts && channel) this.#messageChannelMap.set(ts, channel);
|
|
292
|
+
}
|
|
500
293
|
|
|
501
|
-
|
|
502
|
-
|
|
294
|
+
resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
|
|
295
|
+
const parsed = parseSlackMessageRef(messageId);
|
|
296
|
+
if (parsed) {
|
|
297
|
+
this.trackMessageChannel(parsed.ts, parsed.channel);
|
|
298
|
+
return parsed;
|
|
503
299
|
}
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
return
|
|
300
|
+
if (channelHint) return { channel: channelHint, ts: messageId };
|
|
301
|
+
const channel = this.#messageChannelMap.get(messageId);
|
|
302
|
+
return channel ? { channel, ts: messageId } : null;
|
|
507
303
|
}
|
|
508
304
|
|
|
509
|
-
async
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// Users should use message.$recall() instead, which has the full context
|
|
513
|
-
throw new Error(
|
|
514
|
-
"SlackEndpoint.$recallMessage: Message recall not supported without channel information. " +
|
|
515
|
-
"Use message.$recall() method instead, which contains the required context."
|
|
516
|
-
);
|
|
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);
|
|
517
308
|
}
|
|
518
309
|
|
|
519
|
-
//
|
|
310
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
520
311
|
|
|
521
|
-
/**
|
|
522
|
-
* 邀请用户到频道
|
|
523
|
-
* @param channel 频道 ID
|
|
524
|
-
* @param users 用户 ID 列表
|
|
525
|
-
*/
|
|
526
312
|
async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
this.logger.debug(formatCompact( { op: "invite", endpoint: this.$id, channel, users: users.join(",") }));
|
|
530
|
-
return true;
|
|
531
|
-
} catch (error) {
|
|
532
|
-
this.logger.error(`Slack Endpoint ${this.$id} 邀请用户失败:`, error);
|
|
533
|
-
throw error;
|
|
534
|
-
}
|
|
313
|
+
await this.#client!.conversations.invite({ channel, users: users.join(',') });
|
|
314
|
+
return true;
|
|
535
315
|
}
|
|
536
316
|
|
|
537
|
-
/**
|
|
538
|
-
* 从频道踢出用户
|
|
539
|
-
* @param channel 频道 ID
|
|
540
|
-
* @param user 用户 ID
|
|
541
|
-
*/
|
|
542
317
|
async kickFromChannel(channel: string, user: string): Promise<boolean> {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
this.logger.debug(formatCompact( { op: "kick", endpoint: this.$id, channel, user }));
|
|
546
|
-
return true;
|
|
547
|
-
} catch (error) {
|
|
548
|
-
this.logger.error(`Slack Endpoint ${this.$id} 踢出用户失败:`, error);
|
|
549
|
-
throw error;
|
|
550
|
-
}
|
|
318
|
+
await this.#client!.conversations.kick({ channel, user });
|
|
319
|
+
return true;
|
|
551
320
|
}
|
|
552
321
|
|
|
553
|
-
/**
|
|
554
|
-
* 设置频道话题
|
|
555
|
-
* @param channel 频道 ID
|
|
556
|
-
* @param topic 话题
|
|
557
|
-
*/
|
|
558
322
|
async setChannelTopic(channel: string, topic: string): Promise<boolean> {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
this.logger.debug(formatCompact( { op: "set_topic", endpoint: this.$id, channel }));
|
|
562
|
-
return true;
|
|
563
|
-
} catch (error) {
|
|
564
|
-
this.logger.error(`Slack Endpoint ${this.$id} 设置话题失败:`, error);
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
323
|
+
await this.#client!.conversations.setTopic({ channel, topic });
|
|
324
|
+
return true;
|
|
567
325
|
}
|
|
568
326
|
|
|
569
|
-
/**
|
|
570
|
-
* 设置频道目的
|
|
571
|
-
* @param channel 频道 ID
|
|
572
|
-
* @param purpose 目的
|
|
573
|
-
*/
|
|
574
327
|
async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
this.logger.debug(formatCompact( { op: "set_purpose", endpoint: this.$id, channel }));
|
|
578
|
-
return true;
|
|
579
|
-
} catch (error) {
|
|
580
|
-
this.logger.error(`Slack Endpoint ${this.$id} 设置目的失败:`, error);
|
|
581
|
-
throw error;
|
|
582
|
-
}
|
|
328
|
+
await this.#client!.conversations.setPurpose({ channel, purpose });
|
|
329
|
+
return true;
|
|
583
330
|
}
|
|
584
331
|
|
|
585
|
-
/**
|
|
586
|
-
* 归档频道
|
|
587
|
-
* @param channel 频道 ID
|
|
588
|
-
*/
|
|
589
332
|
async archiveChannel(channel: string): Promise<boolean> {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this.logger.debug(formatCompact( { op: "archive", endpoint: this.$id, channel }));
|
|
593
|
-
return true;
|
|
594
|
-
} catch (error) {
|
|
595
|
-
this.logger.error(`Slack Endpoint ${this.$id} 归档频道失败:`, error);
|
|
596
|
-
throw error;
|
|
597
|
-
}
|
|
333
|
+
await this.#client!.conversations.archive({ channel });
|
|
334
|
+
return true;
|
|
598
335
|
}
|
|
599
336
|
|
|
600
|
-
/**
|
|
601
|
-
* 取消归档频道
|
|
602
|
-
* @param channel 频道 ID
|
|
603
|
-
*/
|
|
604
337
|
async unarchiveChannel(channel: string): Promise<boolean> {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
this.logger.debug(formatCompact( { op: "unarchive", endpoint: this.$id, channel }));
|
|
608
|
-
return true;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
this.logger.error(`Slack Endpoint ${this.$id} 取消归档失败:`, error);
|
|
611
|
-
throw error;
|
|
612
|
-
}
|
|
338
|
+
await this.#client!.conversations.unarchive({ channel });
|
|
339
|
+
return true;
|
|
613
340
|
}
|
|
614
341
|
|
|
615
|
-
/**
|
|
616
|
-
* 重命名频道
|
|
617
|
-
* @param channel 频道 ID
|
|
618
|
-
* @param name 新名称
|
|
619
|
-
*/
|
|
620
342
|
async renameChannel(channel: string, name: string): Promise<boolean> {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
this.logger.debug(formatCompact( { op: "rename", endpoint: this.$id, channel, name }));
|
|
624
|
-
return true;
|
|
625
|
-
} catch (error) {
|
|
626
|
-
this.logger.error(`Slack Endpoint ${this.$id} 重命名频道失败:`, error);
|
|
627
|
-
throw error;
|
|
628
|
-
}
|
|
343
|
+
await this.#client!.conversations.rename({ channel, name });
|
|
344
|
+
return true;
|
|
629
345
|
}
|
|
630
346
|
|
|
631
|
-
/**
|
|
632
|
-
* 获取频道成员列表
|
|
633
|
-
* @param channel 频道 ID
|
|
634
|
-
*/
|
|
635
347
|
async getChannelMembers(channel: string): Promise<string[]> {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return result.members || [];
|
|
639
|
-
} catch (error) {
|
|
640
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取成员列表失败:`, error);
|
|
641
|
-
throw error;
|
|
642
|
-
}
|
|
348
|
+
const result = await this.#client!.conversations.members({ channel });
|
|
349
|
+
return result.members || [];
|
|
643
350
|
}
|
|
644
351
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
*/
|
|
649
|
-
async getChannelInfo(channel: string): Promise<any> {
|
|
650
|
-
try {
|
|
651
|
-
const result = await this.client!.conversations.info({ channel });
|
|
652
|
-
return result.channel;
|
|
653
|
-
} catch (error) {
|
|
654
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取频道信息失败:`, error);
|
|
655
|
-
throw error;
|
|
656
|
-
}
|
|
352
|
+
async getChannelInfo(channel: string): Promise<unknown> {
|
|
353
|
+
const result = await this.#client!.conversations.info({ channel });
|
|
354
|
+
return result.channel;
|
|
657
355
|
}
|
|
658
356
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
*/
|
|
663
|
-
async getUserInfo(user: string): Promise<any> {
|
|
664
|
-
try {
|
|
665
|
-
const result = await this.client!.users.info({ user });
|
|
666
|
-
return result.user;
|
|
667
|
-
} catch (error) {
|
|
668
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取用户信息失败:`, error);
|
|
669
|
-
throw error;
|
|
670
|
-
}
|
|
357
|
+
async getUserInfo(user: string): Promise<unknown> {
|
|
358
|
+
const result = await this.#client!.users.info({ user });
|
|
359
|
+
return result.user;
|
|
671
360
|
}
|
|
672
361
|
|
|
673
|
-
/**
|
|
674
|
-
* 添加消息反应
|
|
675
|
-
* @param channel 频道 ID
|
|
676
|
-
* @param timestamp 消息时间戳
|
|
677
|
-
* @param name 表情名称
|
|
678
|
-
*/
|
|
679
362
|
async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
363
|
+
const reaction = normalizeSlackReactionName(name);
|
|
680
364
|
try {
|
|
681
|
-
await this
|
|
682
|
-
this.logger.debug(formatCompact( { op: "reaction_add", endpoint: this.$id, name }));
|
|
365
|
+
await this.#client!.reactions.add({ channel, timestamp, name: reaction });
|
|
683
366
|
return true;
|
|
684
367
|
} catch (error) {
|
|
685
|
-
|
|
368
|
+
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
369
|
+
if (code === 'already_reacted') return true;
|
|
686
370
|
throw error;
|
|
687
371
|
}
|
|
688
372
|
}
|
|
689
373
|
|
|
690
|
-
/**
|
|
691
|
-
* 移除消息反应
|
|
692
|
-
* @param channel 频道 ID
|
|
693
|
-
* @param timestamp 消息时间戳
|
|
694
|
-
* @param name 表情名称
|
|
695
|
-
*/
|
|
696
374
|
async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
375
|
+
const reaction = normalizeSlackReactionName(name);
|
|
697
376
|
try {
|
|
698
|
-
await this
|
|
699
|
-
this.logger.debug(formatCompact( { op: "reaction_remove", endpoint: this.$id, name }));
|
|
377
|
+
await this.#client!.reactions.remove({ channel, timestamp, name: reaction });
|
|
700
378
|
return true;
|
|
701
379
|
} catch (error) {
|
|
702
|
-
|
|
380
|
+
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
381
|
+
if (code === 'no_reaction') return true;
|
|
703
382
|
throw error;
|
|
704
383
|
}
|
|
705
384
|
}
|
|
706
385
|
|
|
707
|
-
/**
|
|
708
|
-
* 置顶消息
|
|
709
|
-
* @param channel 频道 ID
|
|
710
|
-
* @param timestamp 消息时间戳
|
|
711
|
-
*/
|
|
712
386
|
async pinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
this.logger.debug(formatCompact( { op: "pin", endpoint: this.$id, channel }));
|
|
716
|
-
return true;
|
|
717
|
-
} catch (error) {
|
|
718
|
-
this.logger.error(`Slack Endpoint ${this.$id} 置顶消息失败:`, error);
|
|
719
|
-
throw error;
|
|
720
|
-
}
|
|
387
|
+
await this.#client!.pins.add({ channel, timestamp });
|
|
388
|
+
return true;
|
|
721
389
|
}
|
|
722
390
|
|
|
723
|
-
/**
|
|
724
|
-
* 取消置顶消息
|
|
725
|
-
* @param channel 频道 ID
|
|
726
|
-
* @param timestamp 消息时间戳
|
|
727
|
-
*/
|
|
728
391
|
async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
392
|
+
await this.#client!.pins.remove({ channel, timestamp });
|
|
393
|
+
return true;
|
|
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 };
|
|
737
428
|
}
|
|
429
|
+
return { channel: target };
|
|
738
430
|
}
|