@zhin.js/adapter-slack 1.0.77 → 1.1.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +642 -0
  2. package/README.md +112 -158
  3. package/adapters/slack.js +40 -0
  4. package/adapters/slack.ts +45 -0
  5. package/agent/PERMITS.md +24 -0
  6. package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
  7. package/agent/tools/add_reaction.ts +29 -0
  8. package/agent/tools/archive_channel.ts +21 -0
  9. package/agent/tools/edit_message.ts +25 -0
  10. package/agent/tools/invite_to_channel.ts +23 -0
  11. package/agent/tools/pin_message.ts +23 -0
  12. package/agent/tools/remove_reaction.ts +32 -0
  13. package/agent/tools/set_purpose.ts +23 -0
  14. package/agent/tools/set_topic.ts +23 -0
  15. package/agent/tools/unarchive.ts +21 -0
  16. package/agent/tools/unpin_message.ts +23 -0
  17. package/agent/tools/user_info.ts +29 -0
  18. package/commands/endpoint/add/[id].js +3 -0
  19. package/commands/endpoint/add/[id].ts +3 -0
  20. package/commands/endpoint/list.js +3 -0
  21. package/commands/endpoint/list.ts +3 -0
  22. package/commands/endpoint/remove/[id].js +3 -0
  23. package/commands/endpoint/remove/[id].ts +3 -0
  24. package/lib/client.d.ts +25 -0
  25. package/lib/client.js +2 -0
  26. package/lib/endpoint.d.ts +153 -0
  27. package/lib/endpoint.js +425 -0
  28. package/lib/index.d.ts +10 -10
  29. package/lib/index.js +10 -242
  30. package/lib/markdown-to-mrkdwn.d.ts +9 -0
  31. package/lib/markdown-to-mrkdwn.js +121 -0
  32. package/lib/mrkdwn-to-markdown.d.ts +4 -0
  33. package/lib/mrkdwn-to-markdown.js +30 -0
  34. package/lib/platform-permit.d.ts +15 -0
  35. package/lib/platform-permit.js +43 -0
  36. package/lib/protocol.d.ts +153 -0
  37. package/lib/protocol.js +272 -0
  38. package/lib/side-event-dispatch.d.ts +4 -0
  39. package/lib/side-event-dispatch.js +45 -0
  40. package/lib/slack-endpoint-commands.d.ts +1 -0
  41. package/lib/slack-endpoint-commands.js +18 -0
  42. package/lib/slack-inbound-filter.d.ts +12 -0
  43. package/lib/slack-inbound-filter.js +46 -0
  44. package/lib/slack-message-ref.d.ts +7 -0
  45. package/lib/slack-message-ref.js +17 -0
  46. package/lib/slack-outbound.d.ts +24 -0
  47. package/lib/slack-outbound.js +111 -0
  48. package/lib/slack-reaction.d.ts +1 -0
  49. package/lib/slack-reaction.js +26 -0
  50. package/lib/slack-response-url.d.ts +5 -0
  51. package/lib/slack-response-url.js +16 -0
  52. package/lib/slack-runtime-state.d.ts +1 -0
  53. package/lib/slack-runtime-state.js +6 -0
  54. package/lib/webhook.d.ts +14 -0
  55. package/lib/webhook.js +72 -0
  56. package/package.json +67 -15
  57. package/plugin.js +19 -0
  58. package/schema.json +95 -0
  59. package/src/client.ts +30 -0
  60. package/src/endpoint.ts +526 -0
  61. package/src/index.ts +59 -265
  62. package/src/markdown-to-mrkdwn.ts +117 -0
  63. package/src/mrkdwn-to-markdown.ts +29 -0
  64. package/src/platform-permit.ts +59 -0
  65. package/src/protocol.ts +435 -0
  66. package/src/side-event-dispatch.ts +54 -0
  67. package/src/slack-endpoint-commands.ts +19 -0
  68. package/src/slack-inbound-filter.ts +60 -0
  69. package/src/slack-message-ref.ts +18 -0
  70. package/src/slack-outbound.ts +170 -0
  71. package/src/slack-reaction.ts +23 -0
  72. package/src/slack-response-url.ts +26 -0
  73. package/src/slack-runtime-state.ts +7 -0
  74. package/src/webhook.ts +106 -0
  75. package/lib/adapter.d.ts +0 -16
  76. package/lib/adapter.d.ts.map +0 -1
  77. package/lib/adapter.js +0 -43
  78. package/lib/adapter.js.map +0 -1
  79. package/lib/bot.d.ts +0 -106
  80. package/lib/bot.d.ts.map +0 -1
  81. package/lib/bot.js +0 -628
  82. package/lib/bot.js.map +0 -1
  83. package/lib/index.d.ts.map +0 -1
  84. package/lib/index.js.map +0 -1
  85. package/lib/types.d.ts +0 -17
  86. package/lib/types.d.ts.map +0 -1
  87. package/lib/types.js +0 -2
  88. package/lib/types.js.map +0 -1
  89. package/plugin.yml +0 -3
  90. package/src/adapter.ts +0 -52
  91. package/src/bot.ts +0 -688
  92. package/src/types.ts +0 -18
@@ -0,0 +1,23 @@
1
+ import { defineAgentTool } from '@zhin.js/agent/tools';
2
+ import { z } from 'zod';
3
+ import { platformPermit } from '../../src/platform-permit.js';
4
+
5
+ export default defineAgentTool<{
6
+ channel_id: string;
7
+ timestamp: string;
8
+ }>({
9
+ description: '取消 Slack 频道中消息的置顶',
10
+ inputSchema: z.object({
11
+ channel_id: z.string().describe('频道 ID'),
12
+ timestamp: z.string().describe('消息时间戳'),
13
+ }),
14
+ adapter: 'slack',
15
+ tags: ['slack'],
16
+ permissions: [platformPermit('channel_manager')],
17
+ async execute({ channel_id, timestamp }, context) {
18
+ const client = context.$client;
19
+ await client.pins.remove({ channel: channel_id, timestamp });
20
+ const success = true;
21
+ return { success, message: success ? '已取消置顶' : '操作失败' };
22
+ },
23
+ });
@@ -0,0 +1,29 @@
1
+ import { defineAgentTool } from '@zhin.js/agent/tools';
2
+ import { z } from 'zod';
3
+ import type { SlackUserInfo } from '../../src/client.js';
4
+
5
+ export default defineAgentTool<{
6
+ user_id: string;
7
+ }>({
8
+ description: '查询 Slack 用户详细信息',
9
+ inputSchema: z.object({
10
+ user_id: z.string().describe('用户 ID'),
11
+ }),
12
+ adapter: 'slack',
13
+ tags: ['slack'],
14
+ async execute({ user_id }, context) {
15
+ const client = context.$client;
16
+ const user = (await client.users.info({ user: user_id })).user as SlackUserInfo | undefined;
17
+ if (!user) throw new Error(`Slack 用户不存在: ${user_id}`);
18
+ return {
19
+ id: user.id,
20
+ name: user.name,
21
+ real_name: user.real_name,
22
+ display_name: user.profile?.display_name,
23
+ email: user.profile?.email,
24
+ is_admin: user.is_admin,
25
+ is_bot: user.is_bot,
26
+ status_text: user.profile?.status_text,
27
+ };
28
+ },
29
+ });
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { slackEndpointCommands } from "../../../lib/slack-endpoint-commands.js";
3
+ export default slackEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { slackEndpointCommands } from "../../lib/slack-endpoint-commands.js";
3
+ export default slackEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { slackEndpointCommands } from "../../../lib/slack-endpoint-commands.js";
3
+ export default slackEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.remove;
@@ -0,0 +1,25 @@
1
+ import type { SlackWebClientLike } from './endpoint.js';
2
+ import type { SlackEvent } from './protocol.js';
3
+ /** Slack users.info response fields projected by the bundled tools. */
4
+ export interface SlackUserInfo {
5
+ id?: string;
6
+ name?: string;
7
+ real_name?: string;
8
+ is_admin?: boolean;
9
+ is_bot?: boolean;
10
+ profile?: {
11
+ display_name?: string;
12
+ email?: string;
13
+ status_text?: string;
14
+ };
15
+ }
16
+ export type SlackClientEventMap = Record<string, SlackEvent>;
17
+ declare module '@zhin.js/feature-kit' {
18
+ interface AdapterClientRegistry {
19
+ readonly slack: {
20
+ readonly client: SlackWebClientLike;
21
+ readonly events: SlackClientEventMap;
22
+ };
23
+ }
24
+ }
25
+ export declare const slackClient: import("@zhin.js/adapter").EndpointClientToken<SlackWebClientLike, SlackClientEventMap>;
package/lib/client.js ADDED
@@ -0,0 +1,2 @@
1
+ import { defineEndpointClient } from 'zhin.js/adapter';
2
+ export const slackClient = defineEndpointClient('slack');
@@ -0,0 +1,153 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ import type { EndpointControl, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
3
+ import type { HttpHost } from '@zhin.js/host-http';
4
+ import type { CapabilityId } from 'zhin.js';
5
+ import { type ResolvedSlackConfig, type SlackEvent, type SlackInteractionPayload, type SlackMessageEvent, type SlackSlashCommand } from './protocol.js';
6
+ import { type SlackChatClient } from './slack-outbound.js';
7
+ import { type SlackWebhookHandler } from './webhook.js';
8
+ export interface SlackSocketLike {
9
+ on(event: string, handler: (args: {
10
+ ack: () => Promise<void>;
11
+ body: unknown;
12
+ }) => void | Promise<void>): void;
13
+ start(): Promise<void>;
14
+ disconnect(): Promise<void>;
15
+ }
16
+ export interface SlackWebClientLike extends SlackChatClient {
17
+ auth: {
18
+ test(): Promise<{
19
+ user_id?: string;
20
+ user?: string;
21
+ }>;
22
+ };
23
+ conversations: {
24
+ invite(opts: {
25
+ channel: string;
26
+ users: string;
27
+ }): Promise<unknown>;
28
+ kick(opts: {
29
+ channel: string;
30
+ user: string;
31
+ }): Promise<unknown>;
32
+ setTopic(opts: {
33
+ channel: string;
34
+ topic: string;
35
+ }): Promise<unknown>;
36
+ setPurpose(opts: {
37
+ channel: string;
38
+ purpose: string;
39
+ }): Promise<unknown>;
40
+ archive(opts: {
41
+ channel: string;
42
+ }): Promise<unknown>;
43
+ unarchive(opts: {
44
+ channel: string;
45
+ }): Promise<unknown>;
46
+ rename(opts: {
47
+ channel: string;
48
+ name: string;
49
+ }): Promise<unknown>;
50
+ members(opts: {
51
+ channel: string;
52
+ }): Promise<{
53
+ members?: string[];
54
+ }>;
55
+ info(opts: {
56
+ channel: string;
57
+ }): Promise<{
58
+ channel?: unknown;
59
+ }>;
60
+ list(opts?: {
61
+ types?: string;
62
+ limit?: number;
63
+ cursor?: string;
64
+ exclude_archived?: boolean;
65
+ }): Promise<{
66
+ channels?: unknown[];
67
+ response_metadata?: {
68
+ next_cursor?: string;
69
+ };
70
+ }>;
71
+ };
72
+ users: {
73
+ info(opts: {
74
+ user: string;
75
+ }): Promise<{
76
+ user?: unknown;
77
+ }>;
78
+ list(opts?: {
79
+ limit?: number;
80
+ cursor?: string;
81
+ }): Promise<{
82
+ members?: unknown[];
83
+ response_metadata?: {
84
+ next_cursor?: string;
85
+ };
86
+ }>;
87
+ };
88
+ reactions: {
89
+ add(opts: {
90
+ channel: string;
91
+ timestamp: string;
92
+ name: string;
93
+ }): Promise<unknown>;
94
+ remove(opts: {
95
+ channel: string;
96
+ timestamp: string;
97
+ name: string;
98
+ }): Promise<unknown>;
99
+ };
100
+ pins: {
101
+ add(opts: {
102
+ channel: string;
103
+ timestamp: string;
104
+ }): Promise<unknown>;
105
+ remove(opts: {
106
+ channel: string;
107
+ timestamp: string;
108
+ }): Promise<unknown>;
109
+ };
110
+ chat: SlackChatClient['chat'] & {
111
+ delete(opts: {
112
+ channel: string;
113
+ ts: string;
114
+ }): Promise<unknown>;
115
+ };
116
+ }
117
+ export interface SlackEndpointOptions {
118
+ readonly id: CapabilityId;
119
+ readonly config: ResolvedSlackConfig;
120
+ readonly http?: HttpHost;
121
+ readonly createClient?: (token: string) => SlackWebClientLike;
122
+ readonly createSocket?: (opts: {
123
+ readonly appToken: string;
124
+ readonly clientPingTimeout: number;
125
+ }) => SlackSocketLike;
126
+ }
127
+ export declare class SlackEndpoint extends Endpoint<SlackWebClientLike> implements SlackWebhookHandler {
128
+ #private;
129
+ readonly management: EndpointManagement;
130
+ readonly control: EndpointControl;
131
+ constructor(options: SlackEndpointOptions);
132
+ /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
133
+ get name(): string;
134
+ get client(): SlackWebClientLike;
135
+ get platformUserId(): string | undefined;
136
+ get config(): ResolvedSlackConfig;
137
+ start(): Promise<void>;
138
+ open(): void;
139
+ close(): void;
140
+ stop(): Promise<void>;
141
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
142
+ /** Test / internal: admit a message event when open. */
143
+ admit(event: SlackMessageEvent | SlackEvent): void;
144
+ admitInteraction(payload: SlackInteractionPayload): void;
145
+ admitSlashCommand(cmd: SlackSlashCommand): void;
146
+ handleEnvelope(body: unknown): void;
147
+ trackMessageChannel(ts: string, channel: string): void;
148
+ resolveMessageRef(messageId: string, channelHint?: string): {
149
+ channel: string;
150
+ ts: string;
151
+ } | null;
152
+ recallMessage(messageId: string): Promise<void>;
153
+ }
@@ -0,0 +1,425 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ /**
3
+ * SlackEndpoint — lifecycle, outbound, admit, Socket Mode, agent tool surface.
4
+ */
5
+ import { SocketModeClient } from '@slack/socket-mode';
6
+ import { WebClient } from '@slack/web-api';
7
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
+ import { formatInboundContent, formatInteractionContent, formatSlashContent, resolveSlackChannelType, slackInboundConversation, } from './protocol.js';
9
+ import { createSlackInboundFilterState, shouldDropSlackInboundMessage, } from './slack-inbound-filter.js';
10
+ import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
11
+ import { normalizeSlackReactionName } from './slack-reaction.js';
12
+ import { postSlackEphemeral } from './slack-response-url.js';
13
+ import { editSlackContent, sendSlackContent } from './slack-outbound.js';
14
+ import { registerSlackWebhookRoutes } from './webhook.js';
15
+ import { receiveSlackSideEvent } from './side-event-dispatch.js';
16
+ export class SlackEndpoint extends Endpoint {
17
+ #logger;
18
+ #options;
19
+ #inboundFilter = createSlackInboundFilterState();
20
+ #messageChannelMap = new Map();
21
+ #client;
22
+ #socket;
23
+ #routeReleases = [];
24
+ #botUserId;
25
+ #open = false;
26
+ #started = false;
27
+ management = createSlackEndpointManagement(() => this.client);
28
+ control = Object.freeze({
29
+ recall: async (message) => {
30
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
31
+ if (!ref || !this.#client)
32
+ return;
33
+ await this.#client.chat.delete(ref);
34
+ },
35
+ edit: async (message, content) => {
36
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
37
+ if (!ref)
38
+ return null;
39
+ await this.#editMessage(ref.channel, ref.ts, content);
40
+ return message.id;
41
+ },
42
+ addReaction: async (message, emoji) => {
43
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
44
+ if (!ref)
45
+ return null;
46
+ const reaction = normalizeSlackReactionName(emoji);
47
+ await this.#addReaction(ref.channel, ref.ts, reaction);
48
+ return reaction;
49
+ },
50
+ removeReaction: async (message, reactionId) => {
51
+ const ref = this.resolveMessageRef(message.id, message.conversation.id);
52
+ if (!ref)
53
+ return;
54
+ await this.#removeReaction(ref.channel, ref.ts, reactionId);
55
+ },
56
+ });
57
+ constructor(options) {
58
+ super();
59
+ this.#logger = getAdapterLogger('slack', options.config.id);
60
+ this.#options = options;
61
+ }
62
+ /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
63
+ get name() {
64
+ return this.#options.config.id;
65
+ }
66
+ get client() {
67
+ if (!this.#client)
68
+ throw new Error('Slack client not connected');
69
+ return this.#client;
70
+ }
71
+ get platformUserId() {
72
+ return this.#botUserId;
73
+ }
74
+ get config() {
75
+ return this.#options.config;
76
+ }
77
+ async start() {
78
+ if (this.#started)
79
+ return;
80
+ this.#started = true;
81
+ try {
82
+ const { config } = this.#options;
83
+ this.#client = this.#options.createClient?.(config.token)
84
+ ?? new WebClient(config.token);
85
+ if (config.mode === 'socket') {
86
+ await this.#startSocket();
87
+ }
88
+ else {
89
+ const http = this.#options.http;
90
+ if (!http) {
91
+ throw new Error('Slack HTTP Events API requires httpHostToken (Runtime Host)');
92
+ }
93
+ this.#routeReleases.push(...registerSlackWebhookRoutes(http, this));
94
+ this.#logger.debug(formatCompact({
95
+ endpoint: config.id,
96
+ op: 'webhook',
97
+ path: config.webhookPath,
98
+ }));
99
+ }
100
+ const authTest = await this.#client.auth.test();
101
+ if (authTest.user_id)
102
+ this.#botUserId = String(authTest.user_id);
103
+ this.#logger.info(`connected (${config.mode})`
104
+ + (this.#botUserId ? ` | user: ${this.#botUserId}` : ''));
105
+ }
106
+ catch (error) {
107
+ await this.stop();
108
+ this.#logger.error('Failed to connect Slack endpoint:', error);
109
+ throw error;
110
+ }
111
+ }
112
+ open() {
113
+ this.#open = true;
114
+ }
115
+ close() {
116
+ this.#open = false;
117
+ }
118
+ async stop() {
119
+ this.#open = false;
120
+ if (this.#socket) {
121
+ try {
122
+ await this.#socket.disconnect();
123
+ }
124
+ catch {
125
+ /* ignore */
126
+ }
127
+ this.#socket = undefined;
128
+ }
129
+ for (const release of this.#routeReleases.splice(0))
130
+ release();
131
+ this.#client = undefined;
132
+ this.#started = false;
133
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
134
+ }
135
+ async send({ conversation, payload }) {
136
+ if (!this.#client)
137
+ throw new Error('Slack client not connected');
138
+ const channel = conversation.id;
139
+ const threadTs = conversation.threadId;
140
+ const result = await sendSlackContent(this.#client, payload, { channel, threadTs }, this.#logger);
141
+ this.trackMessageChannel(result.ts, channel);
142
+ return formatSlackMessageRef(channel, result.ts);
143
+ }
144
+ /** Test / internal: admit a message event when open. */
145
+ admit(event) {
146
+ if (!this.#open)
147
+ return;
148
+ void this.#emitPlatformEvent(event.type || 'event', event);
149
+ if (event.type !== 'message' && event.type !== 'app_mention')
150
+ return;
151
+ const msg = event;
152
+ if (shouldDropSlackInboundMessage(msg, this.#inboundFilter, this.#botUserId))
153
+ return;
154
+ if (!msg.channel || !msg.ts)
155
+ return;
156
+ this.trackMessageChannel(msg.ts, msg.channel);
157
+ const threadTs = msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined;
158
+ const conversation = slackInboundConversation(String(this.#options.id), {
159
+ channelId: msg.channel,
160
+ channelType: msg.channel_type,
161
+ threadId: threadTs,
162
+ });
163
+ void this.emit('message.receive', {
164
+ conversation,
165
+ message: { conversation, id: msg.ts },
166
+ content: formatInboundContent(msg),
167
+ sender: { id: msg.user ?? msg.channel ?? '' },
168
+ endpointId: this.#options.config.id,
169
+ ...(msg.type === 'app_mention' ? { mentioned: true } : {}),
170
+ metadata: Object.freeze({
171
+ channelType: resolveSlackChannelType(msg),
172
+ userId: msg.user,
173
+ threadTs,
174
+ ts: msg.ts,
175
+ }),
176
+ }).catch((err) => {
177
+ this.#logger.warn(formatCompact({
178
+ op: 'slack_gateway_receive_failed',
179
+ target: `${conversation.kind}:${conversation.id}`,
180
+ error: err instanceof Error ? err.message : String(err),
181
+ }));
182
+ });
183
+ }
184
+ admitInteraction(payload) {
185
+ if (!this.#open)
186
+ return;
187
+ void this.#emitPlatformEvent(`interaction.${payload.type}`, payload);
188
+ if (payload.type !== 'block_actions' || !payload.actions?.length)
189
+ return;
190
+ const channelId = payload.channel?.id ?? '';
191
+ const userId = payload.user.id;
192
+ const messageTs = payload.message?.ts ?? '';
193
+ if (payload.response_url) {
194
+ postSlackEphemeral(payload.response_url, '已收到', this.#logger);
195
+ }
196
+ const actionTs = payload.actions[0]?.action_ts ?? messageTs ?? `action-${Date.now()}`;
197
+ const conversation = slackInboundConversation(String(this.#options.id), {
198
+ channelId: channelId || userId,
199
+ // block_actions 无 channel_type;无 channel 时按与发起用户的 DM 处理
200
+ channelType: channelId ? undefined : 'im',
201
+ });
202
+ void this.emit('message.receive', {
203
+ conversation,
204
+ message: { conversation, id: actionTs },
205
+ content: formatInteractionContent(payload),
206
+ sender: { id: userId },
207
+ endpointId: this.#options.config.id,
208
+ metadata: Object.freeze({
209
+ eventType: 'block_actions',
210
+ actionId: payload.actions[0]?.action_id,
211
+ threadTs: messageTs || undefined,
212
+ }),
213
+ }).catch((err) => {
214
+ this.#logger.warn(formatCompact({
215
+ op: 'slack_gateway_receive_failed',
216
+ target: `${conversation.kind}:${conversation.id}`,
217
+ error: err instanceof Error ? err.message : String(err),
218
+ }));
219
+ });
220
+ }
221
+ admitSlashCommand(cmd) {
222
+ if (!this.#open)
223
+ return;
224
+ void this.#emitPlatformEvent(`slash.${cmd.command}`, cmd);
225
+ postSlackEphemeral(cmd.response_url, '处理中…', this.#logger);
226
+ const conversation = slackInboundConversation(String(this.#options.id), {
227
+ channelId: cmd.channel_id,
228
+ });
229
+ void this.emit('message.receive', {
230
+ conversation,
231
+ message: { conversation, id: cmd.trigger_id },
232
+ content: formatSlashContent(cmd),
233
+ sender: { id: cmd.user_id, name: cmd.user_name },
234
+ endpointId: this.#options.config.id,
235
+ metadata: Object.freeze({
236
+ eventType: 'slash_command',
237
+ command: cmd.command,
238
+ }),
239
+ }).catch((err) => {
240
+ this.#logger.warn(formatCompact({
241
+ op: 'slack_gateway_receive_failed',
242
+ target: `${conversation.kind}:${conversation.id}`,
243
+ error: err instanceof Error ? err.message : String(err),
244
+ }));
245
+ });
246
+ }
247
+ handleEnvelope(body) {
248
+ const envelope = body;
249
+ if (envelope?.type === 'event_callback' && envelope.event) {
250
+ const event = envelope.event;
251
+ if (event.type !== 'message' && event.type !== 'app_mention') {
252
+ void this.#emitPlatformEvent(event.type || 'event', event);
253
+ receiveSlackSideEvent((name, payload) => this.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
254
+ return;
255
+ }
256
+ this.admit(event);
257
+ }
258
+ }
259
+ trackMessageChannel(ts, channel) {
260
+ if (!ts || !channel)
261
+ return;
262
+ // LRU:超 1024 条淘汰最久未更新的记录,避免无界增长。
263
+ if (this.#messageChannelMap.has(ts))
264
+ this.#messageChannelMap.delete(ts);
265
+ this.#messageChannelMap.set(ts, channel);
266
+ if (this.#messageChannelMap.size > 1024) {
267
+ const oldest = this.#messageChannelMap.keys().next().value;
268
+ if (oldest != null)
269
+ this.#messageChannelMap.delete(oldest);
270
+ }
271
+ }
272
+ resolveMessageRef(messageId, channelHint) {
273
+ const parsed = parseSlackMessageRef(messageId);
274
+ if (parsed) {
275
+ this.trackMessageChannel(parsed.ts, parsed.channel);
276
+ return parsed;
277
+ }
278
+ if (channelHint)
279
+ return { channel: channelHint, ts: messageId };
280
+ const channel = this.#messageChannelMap.get(messageId);
281
+ return channel ? { channel, ts: messageId } : null;
282
+ }
283
+ async recallMessage(messageId) {
284
+ if (!this.#client)
285
+ return;
286
+ const ref = this.resolveMessageRef(messageId);
287
+ if (!ref)
288
+ return;
289
+ await this.#client.chat.delete({ channel: ref.channel, ts: ref.ts });
290
+ }
291
+ async #editMessage(channel, messageTs, content) {
292
+ if (!this.#client)
293
+ throw new Error('Slack client not connected');
294
+ await editSlackContent(this.#client, channel, messageTs, content);
295
+ }
296
+ async #addReaction(channel, timestamp, name) {
297
+ const reaction = normalizeSlackReactionName(name);
298
+ try {
299
+ await this.#client.reactions.add({ channel, timestamp, name: reaction });
300
+ }
301
+ catch (error) {
302
+ const code = error?.data?.error;
303
+ if (code === 'already_reacted')
304
+ return;
305
+ throw error;
306
+ }
307
+ }
308
+ async #removeReaction(channel, timestamp, name) {
309
+ const reaction = normalizeSlackReactionName(name);
310
+ try {
311
+ await this.#client.reactions.remove({ channel, timestamp, name: reaction });
312
+ }
313
+ catch (error) {
314
+ const code = error?.data?.error;
315
+ if (code === 'no_reaction')
316
+ return;
317
+ throw error;
318
+ }
319
+ }
320
+ async #emitPlatformEvent(name, event) {
321
+ await this.emitPlatform(name, event).catch((error) => {
322
+ this.#logger.warn(formatCompact({
323
+ op: 'slack_platform_event_failed',
324
+ event: name,
325
+ error: error instanceof Error ? error.message : String(error),
326
+ }));
327
+ });
328
+ }
329
+ async #startSocket() {
330
+ const { config } = this.#options;
331
+ if (!config.appToken) {
332
+ throw new Error('Slack Socket Mode 需要 appToken(xapp- 前缀的 App-Level Token);注意 bot token 填 token 字段(xoxb- 前缀),两者不可混用');
333
+ }
334
+ if (!config.appToken.startsWith('xapp-')) {
335
+ throw new Error(`Slack appToken 格式不正确:Socket Mode 需要 xapp- 前缀的 App-Level Token(当前看起来是 ${config.appToken.slice(0, 5)}…,xoxb- 是 bot token,请填到 token 字段)`);
336
+ }
337
+ this.#socket = this.#options.createSocket?.({
338
+ appToken: config.appToken,
339
+ clientPingTimeout: config.clientPingTimeout,
340
+ }) ?? new SocketModeClient({
341
+ appToken: config.appToken,
342
+ clientPingTimeout: config.clientPingTimeout,
343
+ });
344
+ this.#socket.on('slack_event', async ({ ack, body }) => {
345
+ await ack();
346
+ this.handleEnvelope(body);
347
+ });
348
+ this.#socket.on('interactive', async ({ ack, body }) => {
349
+ await ack();
350
+ this.admitInteraction(body);
351
+ });
352
+ this.#socket.on('slash_commands', async ({ ack, body }) => {
353
+ await ack();
354
+ this.admitSlashCommand(body);
355
+ });
356
+ await this.#socket.start();
357
+ }
358
+ }
359
+ /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
360
+ const SLACK_LIST_PAGE_SIZE = 1000;
361
+ function createSlackEndpointManagement(requireClient) {
362
+ return Object.freeze({
363
+ // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
364
+ // Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
365
+ async listGroups() {
366
+ const client = requireClient();
367
+ const groups = [];
368
+ let cursor;
369
+ do {
370
+ const page = await client.conversations.list({
371
+ types: 'public_channel',
372
+ exclude_archived: true,
373
+ limit: SLACK_LIST_PAGE_SIZE,
374
+ ...(cursor ? { cursor } : {}),
375
+ });
376
+ for (const value of page.channels ?? []) {
377
+ const channel = asRecord(value);
378
+ const id = typeof channel.id === 'string' ? channel.id : '';
379
+ if (!id)
380
+ continue;
381
+ groups.push({ group_id: id, name: String(channel.name ?? id) });
382
+ }
383
+ cursor = page.response_metadata?.next_cursor || undefined;
384
+ } while (cursor);
385
+ return groups;
386
+ },
387
+ // Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
388
+ async listFriends() {
389
+ const client = requireClient();
390
+ const friends = [];
391
+ let cursor;
392
+ do {
393
+ const page = await client.users.list({
394
+ limit: SLACK_LIST_PAGE_SIZE,
395
+ ...(cursor ? { cursor } : {}),
396
+ });
397
+ for (const value of page.members ?? []) {
398
+ const user = asRecord(value);
399
+ const id = typeof user.id === 'string' ? user.id : '';
400
+ if (!id || user.deleted === true)
401
+ continue;
402
+ const profile = asRecord(user.profile);
403
+ friends.push({
404
+ user_id: id,
405
+ nickname: String(profile.real_name ?? user.real_name ?? user.name ?? id),
406
+ remark: '',
407
+ });
408
+ }
409
+ cursor = page.response_metadata?.next_cursor || undefined;
410
+ } while (cursor);
411
+ return friends;
412
+ },
413
+ // conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
414
+ async listGroupMembers(groupId) {
415
+ const client = requireClient();
416
+ const result = await client.conversations.members({ channel: groupId });
417
+ return result.members ?? [];
418
+ },
419
+ });
420
+ }
421
+ function asRecord(value) {
422
+ return value !== null && typeof value === 'object'
423
+ ? value
424
+ : {};
425
+ }