@zhin.js/adapter-slack 4.0.1 → 4.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 (118) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +207 -98
  3. package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
  4. package/agent/tools/add_reaction.ts +26 -0
  5. package/agent/tools/archive_channel.ts +24 -0
  6. package/agent/tools/edit_message.ts +27 -0
  7. package/agent/tools/invite_to_channel.ts +26 -0
  8. package/agent/tools/pin_message.ts +26 -0
  9. package/agent/tools/remove_reaction.ts +26 -0
  10. package/agent/tools/set_purpose.ts +26 -0
  11. package/agent/tools/set_topic.ts +26 -0
  12. package/agent/tools/unarchive.ts +24 -0
  13. package/agent/tools/unpin_message.ts +26 -0
  14. package/agent/tools/user_info.ts +31 -0
  15. package/lib/agent/tools/add_reaction.js +21 -0
  16. package/lib/agent/tools/add_reaction.js.map +1 -0
  17. package/lib/agent/tools/archive_channel.js +21 -0
  18. package/lib/agent/tools/archive_channel.js.map +1 -0
  19. package/lib/agent/tools/edit_message.js +22 -0
  20. package/lib/agent/tools/edit_message.js.map +1 -0
  21. package/lib/agent/tools/invite_to_channel.js +22 -0
  22. package/lib/agent/tools/invite_to_channel.js.map +1 -0
  23. package/lib/agent/tools/pin_message.js +22 -0
  24. package/lib/agent/tools/pin_message.js.map +1 -0
  25. package/lib/agent/tools/remove_reaction.js +21 -0
  26. package/lib/agent/tools/remove_reaction.js.map +1 -0
  27. package/lib/agent/tools/set_purpose.js +22 -0
  28. package/lib/agent/tools/set_purpose.js.map +1 -0
  29. package/lib/agent/tools/set_topic.js +22 -0
  30. package/lib/agent/tools/set_topic.js.map +1 -0
  31. package/lib/agent/tools/unarchive.js +21 -0
  32. package/lib/agent/tools/unarchive.js.map +1 -0
  33. package/lib/agent/tools/unpin_message.js +22 -0
  34. package/lib/agent/tools/unpin_message.js.map +1 -0
  35. package/lib/agent/tools/user_info.js +28 -0
  36. package/lib/agent/tools/user_info.js.map +1 -0
  37. package/lib/{adapter.js → src/adapter.js} +19 -3
  38. package/lib/src/adapter.js.map +1 -0
  39. package/lib/src/endpoint.js +312 -0
  40. package/lib/src/endpoint.js.map +1 -0
  41. package/lib/src/event-dispatcher.js +233 -0
  42. package/lib/src/event-dispatcher.js.map +1 -0
  43. package/lib/src/index.js +51 -0
  44. package/lib/src/index.js.map +1 -0
  45. package/lib/src/markdown-to-mrkdwn.js +61 -0
  46. package/lib/src/markdown-to-mrkdwn.js.map +1 -0
  47. package/lib/src/mrkdwn-to-markdown.js +31 -0
  48. package/lib/src/mrkdwn-to-markdown.js.map +1 -0
  49. package/lib/{platform-permit.js → src/platform-permit.js} +3 -0
  50. package/lib/src/platform-permit.js.map +1 -0
  51. package/lib/src/segment-mapper.js.map +1 -0
  52. package/lib/src/signing.js +21 -0
  53. package/lib/src/signing.js.map +1 -0
  54. package/lib/src/slack-agent-deps.js +10 -0
  55. package/lib/src/slack-agent-deps.js.map +1 -0
  56. package/lib/src/slack-inbound-filter.js +47 -0
  57. package/lib/src/slack-inbound-filter.js.map +1 -0
  58. package/lib/src/slack-inbound.js +127 -0
  59. package/lib/src/slack-inbound.js.map +1 -0
  60. package/lib/src/slack-message-ref.js +18 -0
  61. package/lib/src/slack-message-ref.js.map +1 -0
  62. package/lib/src/slack-outbound.js +223 -0
  63. package/lib/src/slack-outbound.js.map +1 -0
  64. package/lib/src/slack-reaction.js +27 -0
  65. package/lib/src/slack-reaction.js.map +1 -0
  66. package/lib/src/slack-response-url.js +17 -0
  67. package/lib/src/slack-response-url.js.map +1 -0
  68. package/lib/src/slack-side-events.js +64 -0
  69. package/lib/src/slack-side-events.js.map +1 -0
  70. package/lib/src/transport-http.js +83 -0
  71. package/lib/src/transport-http.js.map +1 -0
  72. package/lib/src/transport-socket.js +68 -0
  73. package/lib/src/transport-socket.js.map +1 -0
  74. package/lib/src/types.js +5 -0
  75. package/lib/src/types.js.map +1 -0
  76. package/package.json +24 -6
  77. package/src/adapter.ts +28 -7
  78. package/src/endpoint.ts +206 -604
  79. package/src/event-dispatcher.ts +255 -0
  80. package/src/index.ts +31 -239
  81. package/src/markdown-to-mrkdwn.ts +58 -0
  82. package/src/mrkdwn-to-markdown.ts +29 -0
  83. package/src/platform-permit.ts +1 -2
  84. package/src/signing.ts +28 -0
  85. package/src/slack-agent-deps.ts +22 -0
  86. package/src/slack-inbound-filter.ts +60 -0
  87. package/src/slack-inbound.ts +140 -0
  88. package/src/slack-message-ref.ts +18 -0
  89. package/src/slack-outbound.ts +269 -0
  90. package/src/slack-reaction.ts +23 -0
  91. package/src/slack-response-url.ts +26 -0
  92. package/src/slack-side-events.ts +74 -0
  93. package/src/transport-http.ts +95 -0
  94. package/src/transport-socket.ts +77 -0
  95. package/src/types.ts +99 -4
  96. package/lib/adapter.d.ts +0 -19
  97. package/lib/adapter.d.ts.map +0 -1
  98. package/lib/adapter.js.map +0 -1
  99. package/lib/endpoint.d.ts +0 -108
  100. package/lib/endpoint.d.ts.map +0 -1
  101. package/lib/endpoint.js +0 -677
  102. package/lib/endpoint.js.map +0 -1
  103. package/lib/index.d.ts +0 -10
  104. package/lib/index.d.ts.map +0 -1
  105. package/lib/index.js +0 -252
  106. package/lib/index.js.map +0 -1
  107. package/lib/platform-permit.d.ts +0 -17
  108. package/lib/platform-permit.d.ts.map +0 -1
  109. package/lib/platform-permit.js.map +0 -1
  110. package/lib/segment-mapper.d.ts +0 -2
  111. package/lib/segment-mapper.d.ts.map +0 -1
  112. package/lib/segment-mapper.js.map +0 -1
  113. package/lib/types.d.ts +0 -17
  114. package/lib/types.d.ts.map +0 -1
  115. package/lib/types.js +0 -2
  116. package/lib/types.js.map +0 -1
  117. /package/{skills/slack → agent}/PERMITS.md +0 -0
  118. /package/lib/{segment-mapper.js → src/segment-mapper.js} +0 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Slack 统一事件分发 — 两种传输模式共用
3
+ */
4
+ import { Message, segment, formatCompact, expandInteractiveSegmentsInContent } from 'zhin.js';
5
+ import { toCanonicalSegments } from './segment-mapper.js';
6
+ import { parseSlackMessageToSegments, resolveSlackChannelType } from './slack-inbound.js';
7
+ import { isSlackNoticeEvent, formatSlackNotice } from './slack-side-events.js';
8
+ import { normalizeSlackSenderForPermit } from './platform-permit.js';
9
+ import type { SlackEvent, SlackMessageEvent, SlackInteractionPayload, SlackSlashCommand } from './types.js';
10
+ import type { SlackEndpoint } from './endpoint.js';
11
+ import { formatSlackMessageRef } from './slack-message-ref.js';
12
+ import { createSlackInboundFilterState, shouldDropSlackInboundMessage } from './slack-inbound-filter.js';
13
+ import { postSlackEphemeral } from './slack-response-url.js';
14
+
15
+ export class SlackEventDispatcher {
16
+ private readonly inboundFilter = createSlackInboundFilterState();
17
+
18
+ constructor(private endpoint: SlackEndpoint) {}
19
+
20
+ private get adapter() { return this.endpoint.adapter; }
21
+ private get logger() { return this.endpoint.logger; }
22
+
23
+ async routeEvent(event: SlackEvent): Promise<void> {
24
+ const type = event.type;
25
+
26
+ if (type === 'message' || type === 'app_mention') {
27
+ await this.handleMessage(event as SlackMessageEvent);
28
+ return;
29
+ }
30
+
31
+ if (type === 'assistant_thread_started') {
32
+ this.handleAssistantThreadStarted(event);
33
+ return;
34
+ }
35
+
36
+ if (type === 'assistant_thread_context_changed') {
37
+ return;
38
+ }
39
+
40
+ if (isSlackNoticeEvent(type)) {
41
+ this.handleNotice(event);
42
+ return;
43
+ }
44
+ }
45
+
46
+ routeInteraction(payload: SlackInteractionPayload): void {
47
+ if (payload.type === 'block_actions' && payload.actions?.length) {
48
+ this.handleBlockAction(payload);
49
+ }
50
+ }
51
+
52
+ routeSlashCommand(cmd: SlackSlashCommand): void {
53
+ this.handleSlashCommand(cmd);
54
+ }
55
+
56
+ private async handleMessage(event: SlackMessageEvent): Promise<void> {
57
+ if (shouldDropSlackInboundMessage(event, this.inboundFilter, this.endpoint.$platformUserId)) {
58
+ return;
59
+ }
60
+
61
+ const messageRef = formatSlackMessageRef(event.channel, event.ts);
62
+ this.endpoint.trackMessageChannel(event.ts, event.channel);
63
+
64
+ const wire = parseSlackMessageToSegments(event);
65
+ const content = toCanonicalSegments(wire);
66
+ const channelType = resolveSlackChannelType(event);
67
+
68
+ const userId = event.user ?? '';
69
+ const userName = (event as any).username ?? (userId || 'Unknown');
70
+
71
+ const message = Message.from(event, {
72
+ $id: messageRef,
73
+ $adapter: 'slack',
74
+ $endpoint: this.endpoint.$config.name,
75
+ $sender: { id: userId, name: userName },
76
+ $channel: { id: event.channel, type: channelType },
77
+ $content: content,
78
+ $raw: event.text ?? '',
79
+ $timestamp: parseFloat(event.ts) * 1000,
80
+ $quote_id: event.thread_ts && event.thread_ts !== event.ts ? event.thread_ts : undefined,
81
+ $recall: async () => {
82
+ await this.endpoint.client!.chat.delete({ channel: event.channel, ts: event.ts });
83
+ },
84
+ $reply: async (replyContent, quote?) => {
85
+ if (!Array.isArray(replyContent)) replyContent = [replyContent];
86
+ const threadTs = quote
87
+ ? (typeof quote === 'boolean' ? event.ts : quote)
88
+ : (event.thread_ts ?? undefined);
89
+ return await this.adapter.sendMessage({
90
+ context: 'slack',
91
+ endpoint: this.endpoint.$config.name,
92
+ id: event.channel,
93
+ type: channelType,
94
+ content: replyContent,
95
+ ...(threadTs ? { threadId: threadTs } : {}),
96
+ });
97
+ },
98
+ });
99
+
100
+ await this.enrichSender(message, event);
101
+ this.adapter.emit('message.receive', message);
102
+ this.logger.debug(
103
+ `${this.endpoint.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`,
104
+ );
105
+ }
106
+
107
+ private handleNotice(event: SlackEvent): void {
108
+ const notice = formatSlackNotice(event, this.endpoint.$config.name);
109
+ this.adapter.emit('notice.receive' as any, notice);
110
+ this.logger.debug(formatCompact({
111
+ notice: `${notice.$scene_type}.${notice.$sub_type}`,
112
+ endpoint: this.endpoint.$id,
113
+ scene: notice.$scene_id,
114
+ }));
115
+ }
116
+
117
+ private handleBlockAction(payload: SlackInteractionPayload): void {
118
+ const action = payload.actions![0];
119
+ const channelId = payload.channel?.id ?? '';
120
+ const userId = payload.user.id;
121
+ const userName = payload.user.username ?? payload.user.name ?? userId;
122
+ const messageTs = payload.message?.ts ?? '';
123
+
124
+ if (payload.response_url) {
125
+ postSlackEphemeral(payload.response_url, '已收到', this.logger);
126
+ }
127
+
128
+ const actionSegment = {
129
+ type: 'action',
130
+ data: {
131
+ id: action.action_id,
132
+ value: action.value ?? '',
133
+ payload: JSON.stringify(action),
134
+ text: action.text?.text ?? '',
135
+ },
136
+ };
137
+
138
+ const message = Message.from(payload as any, {
139
+ $id: action.action_ts ?? messageTs,
140
+ $adapter: 'slack',
141
+ $endpoint: this.endpoint.$config.name,
142
+ $sender: { id: userId, name: userName },
143
+ $channel: { id: channelId, type: 'group' },
144
+ $content: toCanonicalSegments([actionSegment]),
145
+ $raw: action.text?.text ?? '',
146
+ $timestamp: Date.now(),
147
+ $reply: async (content) => {
148
+ if (!Array.isArray(content)) content = [content];
149
+ return await this.adapter.sendMessage({
150
+ context: 'slack',
151
+ endpoint: this.endpoint.$config.name,
152
+ id: channelId,
153
+ type: 'group',
154
+ content,
155
+ ...(messageTs ? { threadId: messageTs } : {}),
156
+ });
157
+ },
158
+ });
159
+
160
+ this.adapter.emit('message.receive', message);
161
+ }
162
+
163
+ private handleSlashCommand(cmd: SlackSlashCommand): void {
164
+ const text = `${cmd.command} ${cmd.text}`.trim();
165
+
166
+ postSlackEphemeral(cmd.response_url, '处理中…', this.logger);
167
+
168
+ const message = Message.from(cmd as any, {
169
+ $id: cmd.trigger_id,
170
+ $adapter: 'slack',
171
+ $endpoint: this.endpoint.$config.name,
172
+ $sender: { id: cmd.user_id, name: cmd.user_name },
173
+ $channel: { id: cmd.channel_id, type: 'group' },
174
+ $content: [{ type: 'text', data: { text } }],
175
+ $raw: text,
176
+ $timestamp: Date.now(),
177
+ $reply: async (content) => {
178
+ if (!Array.isArray(content)) content = [content];
179
+ return await this.adapter.sendMessage({
180
+ context: 'slack',
181
+ endpoint: this.endpoint.$config.name,
182
+ id: cmd.channel_id,
183
+ type: 'group',
184
+ content,
185
+ });
186
+ },
187
+ });
188
+
189
+ this.adapter.emit('message.receive', message);
190
+ }
191
+
192
+ private handleAssistantThreadStarted(event: SlackEvent): void {
193
+ const thread = (event as any).assistant_thread;
194
+ if (!thread) return;
195
+
196
+ const userId = thread.user_id ?? '';
197
+ const channelId = thread.channel_id ?? '';
198
+ const threadTs = thread.thread_ts ?? '';
199
+
200
+ const message = Message.from(event, {
201
+ $id: threadTs,
202
+ $adapter: 'slack',
203
+ $endpoint: this.endpoint.$config.name,
204
+ $sender: { id: userId, name: userId },
205
+ $channel: { id: channelId, type: 'private' },
206
+ $content: [{ type: 'text', data: { text: '' } }],
207
+ $raw: '',
208
+ $timestamp: Date.now(),
209
+ $reply: async (content) => {
210
+ if (!Array.isArray(content)) content = [content];
211
+ return await this.adapter.sendMessage({
212
+ context: 'slack',
213
+ endpoint: this.endpoint.$config.name,
214
+ id: channelId,
215
+ type: 'private',
216
+ content,
217
+ threadId: threadTs,
218
+ });
219
+ },
220
+ });
221
+
222
+ this.adapter.emit('message.receive', message);
223
+ }
224
+
225
+ private async enrichSender(message: Message<any>, event: SlackEvent): Promise<void> {
226
+ if (message.$channel.type !== 'group' || !event.user) return;
227
+ const channelId = event.channel ?? '';
228
+ const userId = event.user;
229
+ const key = `${channelId}:${userId}`;
230
+ const now = Date.now();
231
+ const cached = this.endpoint.senderPermitCache.get(key);
232
+ if (cached && now - cached.at < 60_000) {
233
+ message.$sender.role = cached.role;
234
+ message.$sender.permissions = cached.permissions;
235
+ return;
236
+ }
237
+ try {
238
+ const user = await this.endpoint.getUserInfo(userId);
239
+ let isChannelManager = false;
240
+ try {
241
+ const channel = await this.endpoint.getChannelInfo(channelId);
242
+ if (channel?.creator === userId) isChannelManager = true;
243
+ } catch { /* ignore */ }
244
+ const normalized = normalizeSlackSenderForPermit({
245
+ isWorkspaceOwner: user.is_owner === true,
246
+ isWorkspaceAdmin: user.is_admin === true,
247
+ isChannelManager,
248
+ });
249
+ const entry = { at: now, role: normalized.role, permissions: normalized.permissions ?? [] };
250
+ this.endpoint.senderPermitCache.set(key, entry);
251
+ message.$sender.role = entry.role;
252
+ message.$sender.permissions = entry.permissions;
253
+ } catch { /* conservative deny */ }
254
+ }
255
+ }
package/src/index.ts CHANGED
@@ -1,30 +1,33 @@
1
1
  /**
2
2
  * Slack 适配器入口:类型扩展、导出、注册
3
3
  */
4
- import { usePlugin, type Plugin, type ISceneManagement, createSceneManagementTools, type ToolFeature } from "zhin.js";
5
- import { SlackAdapter } from "./adapter.js";
4
+ import { usePlugin, type Plugin, type ISceneManagement, createSceneManagementTools, type ToolFeature } from 'zhin.js';
5
+ import type { Router } from '@zhin.js/host-router';
6
+ import { SlackAdapter } from './adapter.js';
7
+ import { SlackEventDispatcher } from './event-dispatcher.js';
8
+ import { SlackHttpTransport } from './transport-http.js';
6
9
  import {
7
- platformPermit,
8
10
  registerSlackPlatformPermitChecker,
9
11
  slackGroupPermitResolver,
10
- } from "./platform-permit.js";
12
+ } from './platform-permit.js';
13
+ import { setSlackAgentDeps } from './slack-agent-deps.js';
11
14
 
12
- declare module "zhin.js" {
15
+ declare module 'zhin.js' {
13
16
  interface Adapters {
14
17
  slack: SlackAdapter;
15
18
  }
16
19
  }
17
20
 
18
- export * from "./types.js";
19
- export { SlackEndpoint } from "./endpoint.js";
20
- export { SlackAdapter } from "./adapter.js";
21
+ export * from './types.js';
22
+ export { SlackEndpoint } from './endpoint.js';
23
+ export { SlackAdapter } from './adapter.js';
21
24
 
22
25
  const plugin = usePlugin();
23
26
  const { provide, useContext } = plugin;
24
27
 
25
28
  provide({
26
- name: "slack",
27
- description: "Slack Endpoint Adapter",
29
+ name: 'slack',
30
+ description: 'Slack Endpoint Adapter',
28
31
  mounted: async (p: Plugin) => {
29
32
  const adapter = new SlackAdapter(p);
30
33
  await adapter.start();
@@ -35,15 +38,20 @@ provide({
35
38
  },
36
39
  });
37
40
 
41
+ useContext('router', 'slack', (router: Router, slack: SlackAdapter) => {
42
+ for (const [, endpoint] of slack.endpoints) {
43
+ if (!endpoint.$config.socketMode) {
44
+ const dispatcher = new SlackEventDispatcher(endpoint);
45
+ const transport = new SlackHttpTransport(endpoint.$config, dispatcher, endpoint.logger);
46
+ transport.registerRoutes(router);
47
+ }
48
+ }
49
+ return () => {};
50
+ });
51
+
38
52
  useContext('tool', 'slack', (toolService: ToolFeature, slack: SlackAdapter) => {
39
53
  const disposers: (() => void)[] = [];
40
54
  disposers.push(registerSlackPlatformPermitChecker());
41
- const sceneTools = createSceneManagementTools(
42
- slack as unknown as ISceneManagement,
43
- 'slack',
44
- { permitResolver: slackGroupPermitResolver, registerChecker: false },
45
- );
46
- disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
47
55
 
48
56
  function getEndpoint(endpointId: string) {
49
57
  const endpoint = slack.endpoints.get(endpointId);
@@ -51,230 +59,14 @@ useContext('tool', 'slack', (toolService: ToolFeature, slack: SlackAdapter) => {
51
59
  return endpoint;
52
60
  }
53
61
 
54
- disposers.push(toolService.addTool({
55
- name: 'slack_invite_to_channel',
56
- description: '邀请用户加入 Slack 频道',
57
- parameters: {
58
- type: 'object',
59
- properties: {
60
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
61
- channel: { type: 'string', description: '频道 ID' },
62
- users: { type: 'string', description: '用户 ID 列表(逗号分隔)' },
63
- },
64
- required: ['endpoint_id', 'channel', 'users'],
65
- },
66
- platforms: ['slack'],
67
- tags: ['slack'],
68
- permissions: [platformPermit('channel_manager')],
69
- execute: async (args: Record<string, any>) => {
70
- const endpoint = getEndpoint(args.endpoint_id);
71
- const success = await endpoint.inviteToChannel(args.channel, args.users.split(','));
72
- return { success, message: success ? '已邀请用户加入频道' : '操作失败' };
73
- },
74
- }, plugin.name));
75
-
76
- disposers.push(toolService.addTool({
77
- name: 'slack_set_topic',
78
- description: '设置 Slack 频道话题',
79
- parameters: {
80
- type: 'object',
81
- properties: {
82
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
83
- channel: { type: 'string', description: '频道 ID' },
84
- topic: { type: 'string', description: '新话题' },
85
- },
86
- required: ['endpoint_id', 'channel', 'topic'],
87
- },
88
- platforms: ['slack'],
89
- tags: ['slack'],
90
- permissions: [platformPermit('channel_manager')],
91
- execute: async (args: Record<string, any>) => {
92
- const endpoint = getEndpoint(args.endpoint_id);
93
- const success = await endpoint.setChannelTopic(args.channel, args.topic);
94
- return { success, message: success ? '已设置频道话题' : '操作失败' };
95
- },
96
- }, plugin.name));
97
-
98
- disposers.push(toolService.addTool({
99
- name: 'slack_archive_channel',
100
- description: '归档 Slack 频道',
101
- parameters: {
102
- type: 'object',
103
- properties: {
104
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
105
- channel: { type: 'string', description: '频道 ID' },
106
- },
107
- required: ['endpoint_id', 'channel'],
108
- },
109
- platforms: ['slack'],
110
- tags: ['slack'],
111
- permissions: [platformPermit('workspace_admin')],
112
- execute: async (args: Record<string, any>) => {
113
- const endpoint = getEndpoint(args.endpoint_id);
114
- const success = await endpoint.archiveChannel(args.channel);
115
- return { success, message: success ? '已归档频道' : '操作失败' };
116
- },
117
- }, plugin.name));
118
-
119
- disposers.push(toolService.addTool({
120
- name: 'slack_pin_message',
121
- description: '置顶 Slack 消息',
122
- parameters: {
123
- type: 'object',
124
- properties: {
125
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
126
- channel: { type: 'string', description: '频道 ID' },
127
- timestamp: { type: 'string', description: '消息时间戳' },
128
- },
129
- required: ['endpoint_id', 'channel', 'timestamp'],
130
- },
131
- platforms: ['slack'],
132
- tags: ['slack'],
133
- permissions: [platformPermit('channel_manager')],
134
- execute: async (args: Record<string, any>) => {
135
- const endpoint = getEndpoint(args.endpoint_id);
136
- const success = await endpoint.pinMessage(args.channel, args.timestamp);
137
- return { success, message: success ? '已置顶消息' : '操作失败' };
138
- },
139
- }, plugin.name));
62
+ setSlackAgentDeps({ getEndpoint, getAdapter: () => slack });
140
63
 
141
- disposers.push(toolService.addTool({
142
- name: 'slack_add_reaction',
143
- description: '给 Slack 消息添加表情反应',
144
- parameters: {
145
- type: 'object',
146
- properties: {
147
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
148
- channel: { type: 'string', description: '频道 ID' },
149
- timestamp: { type: 'string', description: '消息时间戳' },
150
- emoji: { type: 'string', description: '表情名称(不含冒号)' },
151
- },
152
- required: ['endpoint_id', 'channel', 'timestamp', 'emoji'],
153
- },
154
- platforms: ['slack'],
155
- tags: ['slack'],
156
- execute: async (args: Record<string, any>) => {
157
- const endpoint = getEndpoint(args.endpoint_id);
158
- const success = await endpoint.addReaction(args.channel, args.timestamp, args.emoji);
159
- return { success, message: success ? `已添加反应 :${args.emoji}:` : '操作失败' };
160
- },
161
- }, plugin.name));
162
-
163
- disposers.push(toolService.addTool({
164
- name: 'slack_remove_reaction',
165
- description: '移除 Slack 消息上的表情反应',
166
- parameters: {
167
- type: 'object',
168
- properties: {
169
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
170
- channel_id: { type: 'string', description: '频道 ID' },
171
- timestamp: { type: 'string', description: '消息时间戳' },
172
- name: { type: 'string', description: '表情名称(如 thumbsup、heart)' },
173
- },
174
- required: ['endpoint_id', 'channel_id', 'timestamp', 'name'],
175
- },
176
- platforms: ['slack'],
177
- tags: ['slack'],
178
- execute: async (args: Record<string, any>) => {
179
- const endpoint = getEndpoint(args.endpoint_id);
180
- const success = await endpoint.removeReaction(args.channel_id, args.timestamp, args.name);
181
- return { success, message: success ? `已移除反应 :${args.name}:` : '操作失败' };
182
- },
183
- }, plugin.name));
184
-
185
- disposers.push(toolService.addTool({
186
- name: 'slack_unpin_message',
187
- description: '取消 Slack 频道中消息的置顶',
188
- parameters: {
189
- type: 'object',
190
- properties: {
191
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
192
- channel_id: { type: 'string', description: '频道 ID' },
193
- timestamp: { type: 'string', description: '消息时间戳' },
194
- },
195
- required: ['endpoint_id', 'channel_id', 'timestamp'],
196
- },
197
- platforms: ['slack'],
198
- tags: ['slack'],
199
- permissions: [platformPermit('channel_manager')],
200
- execute: async (args: Record<string, any>) => {
201
- const endpoint = getEndpoint(args.endpoint_id);
202
- const success = await endpoint.unpinMessage(args.channel_id, args.timestamp);
203
- return { success, message: success ? '已取消置顶' : '操作失败' };
204
- },
205
- }, plugin.name));
206
-
207
- disposers.push(toolService.addTool({
208
- name: 'slack_user_info',
209
- description: '查询 Slack 用户详细信息',
210
- parameters: {
211
- type: 'object',
212
- properties: {
213
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
214
- user_id: { type: 'string', description: '用户 ID' },
215
- },
216
- required: ['endpoint_id', 'user_id'],
217
- },
218
- platforms: ['slack'],
219
- tags: ['slack'],
220
- execute: async (args: Record<string, any>) => {
221
- const endpoint = getEndpoint(args.endpoint_id);
222
- const user = await endpoint.getUserInfo(args.user_id);
223
- return {
224
- id: user.id,
225
- name: user.name,
226
- real_name: user.real_name,
227
- display_name: user.profile?.display_name,
228
- email: user.profile?.email,
229
- is_admin: user.is_admin,
230
- is_bot: user.is_bot,
231
- status_text: user.profile?.status_text,
232
- };
233
- },
234
- }, plugin.name));
235
-
236
- disposers.push(toolService.addTool({
237
- name: 'slack_set_purpose',
238
- description: '设置 Slack 频道的用途/目的',
239
- parameters: {
240
- type: 'object',
241
- properties: {
242
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
243
- channel_id: { type: 'string', description: '频道 ID' },
244
- purpose: { type: 'string', description: '频道用途描述' },
245
- },
246
- required: ['endpoint_id', 'channel_id', 'purpose'],
247
- },
248
- platforms: ['slack'],
249
- tags: ['slack'],
250
- permissions: [platformPermit('channel_manager')],
251
- execute: async (args: Record<string, any>) => {
252
- const endpoint = getEndpoint(args.endpoint_id);
253
- const success = await endpoint.setChannelPurpose(args.channel_id, args.purpose);
254
- return { success, message: success ? '频道用途已更新' : '操作失败' };
255
- },
256
- }, plugin.name));
257
-
258
- disposers.push(toolService.addTool({
259
- name: 'slack_unarchive',
260
- description: '恢复已归档的 Slack 频道',
261
- parameters: {
262
- type: 'object',
263
- properties: {
264
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
265
- channel_id: { type: 'string', description: '频道 ID' },
266
- },
267
- required: ['endpoint_id', 'channel_id'],
268
- },
269
- platforms: ['slack'],
270
- tags: ['slack'],
271
- permissions: [platformPermit('workspace_admin')],
272
- execute: async (args: Record<string, any>) => {
273
- const endpoint = getEndpoint(args.endpoint_id);
274
- const success = await endpoint.unarchiveChannel(args.channel_id);
275
- return { success, message: success ? '频道已恢复' : '操作失败' };
276
- },
277
- }, plugin.name));
64
+ const sceneTools = createSceneManagementTools(
65
+ slack as unknown as ISceneManagement,
66
+ 'slack',
67
+ { permitResolver: slackGroupPermitResolver, registerChecker: false },
68
+ );
69
+ disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
278
70
 
279
71
  return () => disposers.forEach(d => d());
280
72
  });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 常见 Markdown → Slack mrkdwn(Slack 粗体为 *text*,链接为 <url|text>)
3
+ */
4
+ export const SLACK_MRKDWN_TEXT_MAX = 2900;
5
+
6
+ export function markdownToMrkdwn(text: string): string {
7
+ let result = replaceMarkdownLinks(text);
8
+ result = result.replace(/\*\*([^*]+)\*\*/g, '*$1*');
9
+ result = result.replace(/__([^_]+)__/g, '*$1*');
10
+ result = result.replace(/~~([^~]+)~~/g, '~$1~');
11
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
12
+ return result;
13
+ }
14
+
15
+ function replaceMarkdownLinks(text: string): string {
16
+ let out = '';
17
+ let cursor = 0;
18
+ while (cursor < text.length) {
19
+ const labelStart = text.indexOf('[', cursor);
20
+ if (labelStart < 0) break;
21
+ const labelEnd = text.indexOf(']', labelStart + 1);
22
+ if (labelEnd < 0 || text[labelEnd + 1] !== '(') {
23
+ out += text.slice(cursor, labelStart + 1);
24
+ cursor = labelStart + 1;
25
+ continue;
26
+ }
27
+ const urlEnd = text.indexOf(')', labelEnd + 2);
28
+ if (urlEnd < 0) break;
29
+ out += text.slice(cursor, labelStart);
30
+ out += `<${text.slice(labelEnd + 2, urlEnd)}|${text.slice(labelStart + 1, labelEnd)}>`;
31
+ cursor = urlEnd + 1;
32
+ }
33
+ return out + text.slice(cursor);
34
+ }
35
+
36
+ /** 按 Slack section mrkdwn 上限切分(尽量在换行/空格处断开) */
37
+ export function splitMrkdwnText(text: string, maxLen = SLACK_MRKDWN_TEXT_MAX): string[] {
38
+ if (text.length <= maxLen) return [text];
39
+ const chunks: string[] = [];
40
+ let rest = text;
41
+ while (rest.length > maxLen) {
42
+ let cut = rest.lastIndexOf('\n', maxLen);
43
+ if (cut < maxLen * 0.4) cut = rest.lastIndexOf(' ', maxLen);
44
+ if (cut <= 0) cut = maxLen;
45
+ chunks.push(rest.slice(0, cut).trimEnd());
46
+ rest = rest.slice(cut).trimStart();
47
+ }
48
+ if (rest) chunks.push(rest);
49
+ return chunks.length > 0 ? chunks : [text];
50
+ }
51
+
52
+ /** 通知栏 / 无障碍 fallback 用的纯文本 */
53
+ export function mrkdwnToPlainFallback(text: string): string {
54
+ return text
55
+ .replace(/<([^|>]+)\|([^>]+)>/g, '$2 ($1)')
56
+ .replace(/<([^>]+)>/g, '$1')
57
+ .replace(/[*_~`]/g, '');
58
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Slack mrkdwn → 通用 Markdown(入站文本段)
3
+ */
4
+ export function mrkdwnToMarkdown(text: string): string {
5
+ let result = replaceSlackLinks(text);
6
+ result = result.replace(/\*([^*]+)\*/g, '**$1**');
7
+ result = result.replace(/_([^_]+)_/g, '*$1*');
8
+ result = result.replace(/~([^~]+)~/g, '~~$1~~');
9
+ return result;
10
+ }
11
+
12
+ function replaceSlackLinks(text: string): string {
13
+ let out = '';
14
+ let cursor = 0;
15
+ while (cursor < text.length) {
16
+ const start = text.indexOf('<', cursor);
17
+ if (start < 0) break;
18
+ const end = text.indexOf('>', start + 1);
19
+ if (end < 0) break;
20
+ out += text.slice(cursor, start);
21
+ const body = text.slice(start + 1, end);
22
+ const sep = body.indexOf('|');
23
+ const url = sep >= 0 ? body.slice(0, sep) : body;
24
+ const label = sep >= 0 ? body.slice(sep + 1) : url;
25
+ out += url.startsWith('http://') || url.startsWith('https://') ? `[${label}](${url})` : text.slice(start, end + 1);
26
+ cursor = end + 1;
27
+ }
28
+ return out + text.slice(cursor);
29
+ }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Slack platform permit — workspace + channel 权限
3
3
  */
4
- import type { Message } from 'zhin.js';
5
- import { registerPlatformPermitChecker } from 'zhin.js';
4
+ import { registerPlatformPermitChecker, type Message } from 'zhin.js';
6
5
 
7
6
  const ADAPTER = 'slack';
8
7
 
package/src/signing.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Slack 请求签名验证 — HMAC-SHA256
3
+ * https://api.slack.com/authentication/verifying-requests-from-slack
4
+ */
5
+ import { createHmac, timingSafeEqual } from 'node:crypto';
6
+
7
+ const SLACK_VERSION = 'v0';
8
+ const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
9
+
10
+ export function verifySlackSignature(
11
+ signingSecret: string,
12
+ rawBody: string,
13
+ timestamp: string,
14
+ signature: string,
15
+ ): boolean {
16
+ const now = Math.floor(Date.now() / 1000);
17
+ const ts = parseInt(timestamp, 10);
18
+ if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_DRIFT_SECONDS) {
19
+ return false;
20
+ }
21
+
22
+ const baseString = `${SLACK_VERSION}:${timestamp}:${rawBody}`;
23
+ const hmac = createHmac('sha256', signingSecret).update(baseString).digest('hex');
24
+ const expected = `${SLACK_VERSION}=${hmac}`;
25
+
26
+ if (expected.length !== signature.length) return false;
27
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
28
+ }