@zhin.js/adapter-slack 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +58 -250
  3. package/adapters/slack.ts +34 -0
  4. package/agent/tools/add_reaction.ts +2 -2
  5. package/agent/tools/archive_channel.ts +2 -2
  6. package/agent/tools/edit_message.ts +5 -4
  7. package/agent/tools/invite_to_channel.ts +2 -2
  8. package/agent/tools/pin_message.ts +2 -2
  9. package/agent/tools/remove_reaction.ts +2 -2
  10. package/agent/tools/set_purpose.ts +2 -2
  11. package/agent/tools/set_topic.ts +2 -2
  12. package/agent/tools/unarchive.ts +2 -2
  13. package/agent/tools/unpin_message.ts +2 -2
  14. package/agent/tools/user_info.ts +2 -2
  15. package/lib/endpoint.d.ts +147 -0
  16. package/lib/endpoint.js +335 -0
  17. package/lib/index.d.ts +11 -0
  18. package/lib/index.js +11 -0
  19. package/lib/markdown-to-mrkdwn.d.ts +9 -0
  20. package/lib/markdown-to-mrkdwn.js +121 -0
  21. package/lib/mrkdwn-to-markdown.d.ts +4 -0
  22. package/lib/{src/mrkdwn-to-markdown.js → mrkdwn-to-markdown.js} +0 -1
  23. package/lib/platform-permit.d.ts +16 -0
  24. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  25. package/lib/protocol.d.ts +143 -0
  26. package/lib/protocol.js +225 -0
  27. package/lib/slack-agent-deps.d.ts +28 -0
  28. package/lib/slack-agent-deps.js +30 -0
  29. package/lib/slack-inbound-filter.d.ts +12 -0
  30. package/lib/{src/slack-inbound-filter.js → slack-inbound-filter.js} +0 -1
  31. package/lib/slack-message-ref.d.ts +7 -0
  32. package/lib/{src/slack-message-ref.js → slack-message-ref.js} +0 -1
  33. package/lib/slack-outbound.d.ts +24 -0
  34. package/lib/slack-outbound.js +109 -0
  35. package/lib/slack-reaction.d.ts +1 -0
  36. package/lib/{src/slack-reaction.js → slack-reaction.js} +0 -1
  37. package/lib/slack-response-url.d.ts +5 -0
  38. package/lib/{src/slack-response-url.js → slack-response-url.js} +0 -1
  39. package/lib/webhook.d.ts +14 -0
  40. package/lib/webhook.js +69 -0
  41. package/package.json +41 -20
  42. package/plugin.ts +13 -0
  43. package/schema.json +38 -0
  44. package/src/endpoint.ts +317 -227
  45. package/src/index.ts +62 -66
  46. package/src/markdown-to-mrkdwn.ts +63 -4
  47. package/src/platform-permit.ts +1 -1
  48. package/src/protocol.ts +380 -0
  49. package/src/slack-agent-deps.ts +46 -11
  50. package/src/slack-inbound-filter.ts +1 -1
  51. package/src/slack-outbound.ts +32 -134
  52. package/src/webhook.ts +103 -0
  53. package/lib/agent/tools/add_reaction.js +0 -21
  54. package/lib/agent/tools/add_reaction.js.map +0 -1
  55. package/lib/agent/tools/archive_channel.js +0 -21
  56. package/lib/agent/tools/archive_channel.js.map +0 -1
  57. package/lib/agent/tools/edit_message.js +0 -22
  58. package/lib/agent/tools/edit_message.js.map +0 -1
  59. package/lib/agent/tools/invite_to_channel.js +0 -22
  60. package/lib/agent/tools/invite_to_channel.js.map +0 -1
  61. package/lib/agent/tools/pin_message.js +0 -22
  62. package/lib/agent/tools/pin_message.js.map +0 -1
  63. package/lib/agent/tools/remove_reaction.js +0 -21
  64. package/lib/agent/tools/remove_reaction.js.map +0 -1
  65. package/lib/agent/tools/set_purpose.js +0 -22
  66. package/lib/agent/tools/set_purpose.js.map +0 -1
  67. package/lib/agent/tools/set_topic.js +0 -22
  68. package/lib/agent/tools/set_topic.js.map +0 -1
  69. package/lib/agent/tools/unarchive.js +0 -21
  70. package/lib/agent/tools/unarchive.js.map +0 -1
  71. package/lib/agent/tools/unpin_message.js +0 -22
  72. package/lib/agent/tools/unpin_message.js.map +0 -1
  73. package/lib/agent/tools/user_info.js +0 -28
  74. package/lib/agent/tools/user_info.js.map +0 -1
  75. package/lib/src/adapter.js +0 -62
  76. package/lib/src/adapter.js.map +0 -1
  77. package/lib/src/endpoint.js +0 -312
  78. package/lib/src/endpoint.js.map +0 -1
  79. package/lib/src/event-dispatcher.js +0 -233
  80. package/lib/src/event-dispatcher.js.map +0 -1
  81. package/lib/src/index.js +0 -51
  82. package/lib/src/index.js.map +0 -1
  83. package/lib/src/markdown-to-mrkdwn.js +0 -61
  84. package/lib/src/markdown-to-mrkdwn.js.map +0 -1
  85. package/lib/src/mrkdwn-to-markdown.js.map +0 -1
  86. package/lib/src/platform-permit.js.map +0 -1
  87. package/lib/src/segment-mapper.js +0 -2
  88. package/lib/src/segment-mapper.js.map +0 -1
  89. package/lib/src/signing.js +0 -21
  90. package/lib/src/signing.js.map +0 -1
  91. package/lib/src/slack-agent-deps.js +0 -10
  92. package/lib/src/slack-agent-deps.js.map +0 -1
  93. package/lib/src/slack-inbound-filter.js.map +0 -1
  94. package/lib/src/slack-inbound.js +0 -127
  95. package/lib/src/slack-inbound.js.map +0 -1
  96. package/lib/src/slack-message-ref.js.map +0 -1
  97. package/lib/src/slack-outbound.js +0 -223
  98. package/lib/src/slack-outbound.js.map +0 -1
  99. package/lib/src/slack-reaction.js.map +0 -1
  100. package/lib/src/slack-response-url.js.map +0 -1
  101. package/lib/src/slack-side-events.js +0 -64
  102. package/lib/src/slack-side-events.js.map +0 -1
  103. package/lib/src/transport-http.js +0 -83
  104. package/lib/src/transport-http.js.map +0 -1
  105. package/lib/src/transport-socket.js +0 -68
  106. package/lib/src/transport-socket.js.map +0 -1
  107. package/lib/src/types.js +0 -5
  108. package/lib/src/types.js.map +0 -1
  109. package/plugin.yml +0 -3
  110. package/src/adapter.ts +0 -75
  111. package/src/event-dispatcher.ts +0 -255
  112. package/src/segment-mapper.ts +0 -1
  113. package/src/signing.ts +0 -28
  114. package/src/slack-inbound.ts +0 -140
  115. package/src/slack-side-events.ts +0 -74
  116. package/src/transport-http.ts +0 -95
  117. package/src/transport-socket.ts +0 -77
  118. package/src/types.ts +0 -113
@@ -0,0 +1,143 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
3
+ export interface SlackAdapterConfig {
4
+ readonly name?: string;
5
+ readonly token?: string;
6
+ readonly signingSecret?: string;
7
+ readonly appToken?: string;
8
+ /** Default true (Socket Mode). Set false for HTTP Events API via httpHostToken. */
9
+ readonly socketMode?: boolean;
10
+ readonly webhookPath?: string;
11
+ readonly clientPingTimeout?: number;
12
+ /** Transitional: legacy root `endpoints[]` with `context: slack`. */
13
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedSlackConfig> & {
14
+ readonly context?: string;
15
+ readonly socketMode?: boolean;
16
+ readonly signingSecret?: string;
17
+ readonly appToken?: string;
18
+ readonly webhookPath?: string;
19
+ readonly clientPingTimeout?: number;
20
+ }>;
21
+ }
22
+ export interface ResolvedSlackConfig {
23
+ readonly context: 'slack';
24
+ readonly name: string;
25
+ readonly token: string;
26
+ readonly mode: 'socket' | 'http';
27
+ readonly signingSecret: string;
28
+ readonly appToken?: string;
29
+ readonly webhookPath: string;
30
+ readonly clientPingTimeout: number;
31
+ }
32
+ export interface SlackEventEnvelope {
33
+ readonly token?: string;
34
+ readonly team_id?: string;
35
+ readonly api_app_id?: string;
36
+ readonly event: SlackEvent;
37
+ readonly type: 'event_callback';
38
+ readonly event_id?: string;
39
+ readonly event_time?: number;
40
+ }
41
+ export interface SlackUrlVerification {
42
+ readonly type: 'url_verification';
43
+ readonly token: string;
44
+ readonly challenge: string;
45
+ }
46
+ export interface SlackInteractionPayload {
47
+ readonly type: 'block_actions' | 'message_action' | 'shortcut' | 'view_submission' | 'view_closed';
48
+ readonly trigger_id?: string;
49
+ readonly user: {
50
+ id: string;
51
+ username?: string;
52
+ name?: string;
53
+ team_id?: string;
54
+ };
55
+ readonly channel?: {
56
+ id: string;
57
+ name?: string;
58
+ };
59
+ readonly message?: {
60
+ ts: string;
61
+ text?: string;
62
+ [key: string]: unknown;
63
+ };
64
+ readonly actions?: SlackBlockAction[];
65
+ readonly response_url?: string;
66
+ readonly [key: string]: unknown;
67
+ }
68
+ export interface SlackBlockAction {
69
+ readonly type: string;
70
+ readonly action_id: string;
71
+ readonly block_id: string;
72
+ readonly value?: string;
73
+ readonly text?: {
74
+ type: string;
75
+ text: string;
76
+ };
77
+ readonly action_ts?: string;
78
+ readonly [key: string]: unknown;
79
+ }
80
+ export interface SlackSlashCommand {
81
+ readonly token: string;
82
+ readonly team_id: string;
83
+ readonly channel_id: string;
84
+ readonly channel_name: string;
85
+ readonly user_id: string;
86
+ readonly user_name: string;
87
+ readonly command: string;
88
+ readonly text: string;
89
+ readonly response_url: string;
90
+ readonly trigger_id: string;
91
+ readonly api_app_id?: string;
92
+ }
93
+ export interface SlackEvent {
94
+ readonly type: string;
95
+ readonly ts?: string;
96
+ readonly event_ts?: string;
97
+ readonly user?: string;
98
+ readonly channel?: string;
99
+ readonly channel_type?: string;
100
+ readonly text?: string;
101
+ readonly thread_ts?: string;
102
+ readonly subtype?: string;
103
+ readonly bot_id?: string;
104
+ readonly [key: string]: unknown;
105
+ }
106
+ export type SlackMessageEvent = SlackEvent & {
107
+ readonly type: 'message' | 'app_mention';
108
+ readonly ts: string;
109
+ readonly channel: string;
110
+ };
111
+ export interface SlackWireSegment {
112
+ readonly type: string;
113
+ readonly data?: Record<string, unknown>;
114
+ }
115
+ export declare function resolveSlackConfig(config?: SlackAdapterConfig): ResolvedSlackConfig;
116
+ export declare function normalizeWebhookPath(path: string): string;
117
+ export declare function resolveSlackChannelType(event: Pick<SlackEvent, 'channel_type'>): 'private' | 'group';
118
+ /** Build inbound text for MessageGateway.receive. */
119
+ export declare function formatInboundContent(event: SlackMessageEvent | SlackEvent): string;
120
+ export declare function formatInteractionContent(payload: SlackInteractionPayload): string;
121
+ export declare function formatSlashContent(cmd: SlackSlashCommand): string;
122
+ export declare function inboundMessageId(event: SlackMessageEvent): string;
123
+ /**
124
+ * Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
125
+ * Segment canonicalization is intentionally not done here.
126
+ */
127
+ export declare function formatOutboundWire(payload: unknown): {
128
+ text: string;
129
+ blocks: Record<string, unknown>[];
130
+ attachments: Record<string, unknown>[];
131
+ files: Array<{
132
+ buffer?: Buffer;
133
+ url?: string;
134
+ path?: string;
135
+ name?: string;
136
+ }>;
137
+ };
138
+ export declare function keyboardToBlockKitBlocks(data: Record<string, unknown>): Record<string, unknown>[];
139
+ export declare function verifySlackSignature(signingSecret: string, rawBody: string, timestamp: string, signature: string): boolean;
140
+ export declare function readTextBody(request: IncomingMessage, options?: {
141
+ readonly limit?: number;
142
+ }): Promise<string>;
143
+ export declare function headerValue(headers: IncomingMessage['headers'], name: string): string;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Slack protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { createHmac, timingSafeEqual } from 'node:crypto';
6
+ import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
7
+ const SLACK_SIG_VERSION = 'v0';
8
+ const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
9
+ export function resolveSlackConfig(config = {}) {
10
+ const entry = config.endpoints?.find((item) => item.context === 'slack');
11
+ const token = config.token
12
+ ?? entry?.token
13
+ ?? process.env.SLACK_BOT_TOKEN
14
+ ?? process.env.SLACK_TOKEN;
15
+ if (!token) {
16
+ throw new TypeError('Slack adapter requires token (plugins.<key>.token or endpoints with context: slack)');
17
+ }
18
+ const name = (typeof config.name === 'string' && config.name)
19
+ || (typeof entry?.name === 'string' && entry.name)
20
+ || process.env.SLACK_BOT_NAME
21
+ || 'slack-bot';
22
+ const socketMode = config.socketMode ?? entry?.socketMode;
23
+ // Prefer Socket Mode (default true) — no public URL required.
24
+ const mode = socketMode === false ? 'http' : 'socket';
25
+ const signingSecret = config.signingSecret
26
+ ?? entry?.signingSecret
27
+ ?? process.env.SLACK_SIGNING_SECRET
28
+ ?? '';
29
+ const appToken = config.appToken
30
+ ?? entry?.appToken
31
+ ?? process.env.SLACK_APP_TOKEN
32
+ ?? undefined;
33
+ if (mode === 'socket' && !appToken) {
34
+ throw new TypeError('Slack Socket Mode requires appToken (xapp-...); set socketMode: false for HTTP Events API');
35
+ }
36
+ if (mode === 'http' && !signingSecret) {
37
+ throw new TypeError('Slack HTTP Events API requires signingSecret');
38
+ }
39
+ return {
40
+ context: 'slack',
41
+ name,
42
+ token,
43
+ mode,
44
+ signingSecret,
45
+ appToken,
46
+ webhookPath: normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? '/slack/events'),
47
+ clientPingTimeout: config.clientPingTimeout
48
+ ?? entry?.clientPingTimeout
49
+ ?? 15_000,
50
+ };
51
+ }
52
+ export function normalizeWebhookPath(path) {
53
+ const trimmed = path.trim() || '/slack/events';
54
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
55
+ }
56
+ export function resolveSlackChannelType(event) {
57
+ return event.channel_type === 'im' ? 'private' : 'group';
58
+ }
59
+ /** Build inbound text for MessageGateway.receive. */
60
+ export function formatInboundContent(event) {
61
+ const text = typeof event.text === 'string' ? event.text : '';
62
+ if (text)
63
+ return mrkdwnToMarkdown(text);
64
+ if ('files' in event && Array.isArray(event.files) && event.files.length > 0) {
65
+ return '[file]';
66
+ }
67
+ return '';
68
+ }
69
+ export function formatInteractionContent(payload) {
70
+ const action = payload.actions?.[0];
71
+ if (!action)
72
+ return '[action]';
73
+ const label = action.text?.text ?? action.value ?? action.action_id;
74
+ return `[action: ${action.action_id}${label ? ` ${label}` : ''}]`;
75
+ }
76
+ export function formatSlashContent(cmd) {
77
+ return `${cmd.command} ${cmd.text}`.trim();
78
+ }
79
+ export function inboundMessageId(event) {
80
+ return `${event.channel}:${event.ts}`;
81
+ }
82
+ /**
83
+ * Wire-encode an already-rendered outbound payload into Slack text + Block Kit blocks.
84
+ * Segment canonicalization is intentionally not done here.
85
+ */
86
+ export function formatOutboundWire(payload) {
87
+ if (typeof payload === 'string') {
88
+ return { text: payload, blocks: [], attachments: [], files: [] };
89
+ }
90
+ const items = Array.isArray(payload)
91
+ ? payload
92
+ : payload && typeof payload === 'object' && 'type' in payload
93
+ ? [payload]
94
+ : [];
95
+ if (items.length === 0) {
96
+ const text = payload == null
97
+ ? ''
98
+ : typeof payload === 'object'
99
+ ? JSON.stringify(payload)
100
+ : String(payload);
101
+ return { text, blocks: [], attachments: [], files: [] };
102
+ }
103
+ let text = '';
104
+ const blocks = [];
105
+ const attachments = [];
106
+ const files = [];
107
+ for (const item of items) {
108
+ if (typeof item === 'string') {
109
+ text += item;
110
+ continue;
111
+ }
112
+ const data = item.data ?? {};
113
+ switch (item.type) {
114
+ case 'text':
115
+ text += String(data.text ?? data.content ?? '');
116
+ break;
117
+ case 'at':
118
+ case 'mention':
119
+ text += `<@${String(data.id ?? data.target ?? '')}>`;
120
+ break;
121
+ case 'channel_mention':
122
+ text += `<#${String(data.id ?? '')}>`;
123
+ break;
124
+ case 'link':
125
+ if (data.text && data.text !== data.url) {
126
+ text += `<${String(data.url)}|${String(data.text)}>`;
127
+ }
128
+ else {
129
+ text += `<${String(data.url ?? '')}>`;
130
+ }
131
+ break;
132
+ case 'image':
133
+ if (typeof data.url === 'string' && data.url) {
134
+ attachments.push({
135
+ image_url: data.url,
136
+ title: String(data.name ?? data.title ?? ''),
137
+ });
138
+ }
139
+ else if (data.media && typeof data.media === 'object') {
140
+ files.push(resolveMediaToFile(data.media, String(data.alt ?? 'image')));
141
+ }
142
+ break;
143
+ case 'audio':
144
+ case 'video':
145
+ case 'file':
146
+ if (data.media && typeof data.media === 'object') {
147
+ files.push(resolveMediaToFile(data.media, String(data.name ?? item.type)));
148
+ }
149
+ else if (data.file || data.url) {
150
+ files.push({
151
+ path: typeof data.file === 'string' ? data.file : undefined,
152
+ url: typeof data.url === 'string' ? data.url : undefined,
153
+ name: String(data.name ?? item.type),
154
+ });
155
+ }
156
+ break;
157
+ case 'keyboard':
158
+ blocks.push(...keyboardToBlockKitBlocks(data));
159
+ break;
160
+ default:
161
+ text += String(data.text ?? `[${item.type}]`);
162
+ }
163
+ }
164
+ return { text, blocks, attachments, files };
165
+ }
166
+ export function keyboardToBlockKitBlocks(data) {
167
+ const rows = data.rows;
168
+ if (!rows?.length)
169
+ return [];
170
+ const blocks = [];
171
+ for (const row of rows) {
172
+ const elements = row.slice(0, 5).map((btn, index) => ({
173
+ type: 'button',
174
+ text: { type: 'plain_text', text: String(btn.label ?? btn.text ?? 'button').slice(0, 75) },
175
+ action_id: String(btn.id ?? btn.action_id ?? `btn_${blocks.length}_${index}`),
176
+ ...(btn.value != null ? { value: String(btn.value) } : {}),
177
+ ...(btn.style === 'primary' ? { style: 'primary' } : {}),
178
+ ...(btn.style === 'danger' ? { style: 'danger' } : {}),
179
+ }));
180
+ if (elements.length > 0) {
181
+ blocks.push({ type: 'actions', elements });
182
+ }
183
+ }
184
+ return blocks;
185
+ }
186
+ function resolveMediaToFile(media, name) {
187
+ if (media.kind === 'base64') {
188
+ return { buffer: Buffer.from(media.value, 'base64'), name };
189
+ }
190
+ return { url: media.value, name };
191
+ }
192
+ export function verifySlackSignature(signingSecret, rawBody, timestamp, signature) {
193
+ const now = Math.floor(Date.now() / 1000);
194
+ const ts = parseInt(timestamp, 10);
195
+ if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_DRIFT_SECONDS) {
196
+ return false;
197
+ }
198
+ const baseString = `${SLACK_SIG_VERSION}:${timestamp}:${rawBody}`;
199
+ const hmac = createHmac('sha256', signingSecret).update(baseString).digest('hex');
200
+ const expected = `${SLACK_SIG_VERSION}=${hmac}`;
201
+ if (expected.length !== signature.length)
202
+ return false;
203
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
204
+ }
205
+ export async function readTextBody(request, options = {}) {
206
+ const limit = options.limit ?? 1_048_576;
207
+ const chunks = [];
208
+ let size = 0;
209
+ for await (const chunk of request) {
210
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
211
+ size += buffer.length;
212
+ if (size > limit) {
213
+ request.destroy();
214
+ throw new Error(`Request body exceeds ${limit} bytes`);
215
+ }
216
+ chunks.push(buffer);
217
+ }
218
+ return Buffer.concat(chunks).toString('utf8');
219
+ }
220
+ export function headerValue(headers, name) {
221
+ const raw = headers[name.toLowerCase()];
222
+ if (Array.isArray(raw))
223
+ return raw[0] ?? '';
224
+ return raw ?? '';
225
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Agent tool deps for slack.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ export interface SlackAgentEndpoint {
6
+ inviteToChannel(channel: string, users: string[]): Promise<boolean>;
7
+ kickFromChannel(channel: string, user: string): Promise<boolean>;
8
+ setChannelTopic(channel: string, topic: string): Promise<boolean>;
9
+ setChannelPurpose(channel: string, purpose: string): Promise<boolean>;
10
+ archiveChannel(channel: string): Promise<boolean>;
11
+ unarchiveChannel(channel: string): Promise<boolean>;
12
+ renameChannel(channel: string, name: string): Promise<boolean>;
13
+ getChannelMembers(channel: string): Promise<string[]>;
14
+ getChannelInfo(channel: string): Promise<unknown>;
15
+ getUserInfo(user: string): Promise<unknown>;
16
+ addReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
17
+ removeReaction(channel: string, timestamp: string, name: string): Promise<boolean>;
18
+ pinMessage(channel: string, timestamp: string): Promise<boolean>;
19
+ unpinMessage(channel: string, timestamp: string): Promise<boolean>;
20
+ editMessage(channel: string, messageTs: string, content: unknown): Promise<void>;
21
+ }
22
+ export interface SlackAgentDeps {
23
+ getEndpoint: (endpointId: string) => SlackAgentEndpoint;
24
+ }
25
+ export declare function registerSlackAgentEndpoint(endpointId: string, endpoint: SlackAgentEndpoint): () => void;
26
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
27
+ export declare function setSlackAgentDeps(deps: SlackAgentDeps | null): void;
28
+ export declare function getSlackAgentDeps(): SlackAgentDeps;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent tool deps for slack.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ const endpoints = new Map();
6
+ let override = null;
7
+ export function registerSlackAgentEndpoint(endpointId, endpoint) {
8
+ endpoints.set(endpointId, endpoint);
9
+ return () => {
10
+ if (endpoints.get(endpointId) === endpoint) {
11
+ endpoints.delete(endpointId);
12
+ }
13
+ };
14
+ }
15
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
+ export function setSlackAgentDeps(deps) {
17
+ override = deps;
18
+ }
19
+ export function getSlackAgentDeps() {
20
+ if (override)
21
+ return override;
22
+ return {
23
+ getEndpoint(endpointId) {
24
+ const registered = endpoints.get(endpointId);
25
+ if (!registered)
26
+ throw new Error(`Endpoint ${endpointId} 不存在`);
27
+ return registered;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { SlackMessageEvent } from './protocol.js';
2
+ export interface SlackInboundFilterState {
3
+ seenInbound: Map<string, number>;
4
+ }
5
+ export declare function createSlackInboundFilterState(): SlackInboundFilterState;
6
+ /**
7
+ * 过滤不应进入 IM 管道的 Slack 入站消息。
8
+ * - 跳过 Bot 自身消息
9
+ * - 频道内 @Bot:只保留 app_mention,丢弃重复的 message 事件
10
+ * - channel:ts 去重(message + app_mention 等同一条)
11
+ */
12
+ export declare function shouldDropSlackInboundMessage(event: SlackMessageEvent, state: SlackInboundFilterState, botUserId?: string): boolean;
@@ -44,4 +44,3 @@ export function shouldDropSlackInboundMessage(event, state, botUserId) {
44
44
  pruneSeen(state, now);
45
45
  return false;
46
46
  }
47
- //# sourceMappingURL=slack-inbound-filter.js.map
@@ -0,0 +1,7 @@
1
+ /** Slack 消息定位:channel + ts(Activity Feedback / recall / reaction 共用) */
2
+ export declare function formatSlackMessageRef(channel: string, ts: string): string;
3
+ export declare function parseSlackMessageRef(ref: string): {
4
+ channel: string;
5
+ ts: string;
6
+ } | null;
7
+ export declare function slackMessageTs(ref: string): string;
@@ -15,4 +15,3 @@ export function parseSlackMessageRef(ref) {
15
15
  export function slackMessageTs(ref) {
16
16
  return parseSlackMessageRef(ref)?.ts ?? ref;
17
17
  }
18
- //# sourceMappingURL=slack-message-ref.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Slack outbound — chat.postMessage + Block Kit + files.uploadV2
3
+ */
4
+ import type { Logger } from '@zhin.js/logger';
5
+ import { keyboardToBlockKitBlocks } from './protocol.js';
6
+ export interface SlackOutboundResult {
7
+ ts: string;
8
+ }
9
+ export interface SlackSendOptions {
10
+ channel: string;
11
+ threadTs?: string;
12
+ }
13
+ export interface SlackChatClient {
14
+ chat: {
15
+ postMessage(opts: Record<string, unknown>): Promise<{
16
+ ts?: string;
17
+ }>;
18
+ update(opts: Record<string, unknown>): Promise<unknown>;
19
+ };
20
+ filesUploadV2?(opts: Record<string, unknown>): Promise<unknown>;
21
+ }
22
+ export declare function sendSlackContent(client: SlackChatClient, content: unknown, opts: SlackSendOptions, logger: Logger): Promise<SlackOutboundResult>;
23
+ export declare function editSlackContent(client: SlackChatClient, channel: string, ts: string, content: unknown): Promise<void>;
24
+ export { keyboardToBlockKitBlocks };
@@ -0,0 +1,109 @@
1
+ import { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
2
+ import { formatOutboundWire, keyboardToBlockKitBlocks } from './protocol.js';
3
+ const SLACK_MAX_BLOCKS_PER_MESSAGE = 48;
4
+ export async function sendSlackContent(client, content, opts, logger) {
5
+ const wire = formatOutboundWire(content);
6
+ const blocks = [...wire.blocks];
7
+ const textDelivery = applyTextMrkdwnBlocks(wire.text, blocks);
8
+ for (const pf of wire.files) {
9
+ try {
10
+ await uploadFile(client, opts.channel, pf, opts.threadTs, logger);
11
+ }
12
+ catch (e) {
13
+ logger.error('Failed to upload file:', e);
14
+ }
15
+ }
16
+ return postSlackMessage(client, {
17
+ channel: opts.channel,
18
+ threadTs: opts.threadTs,
19
+ blocks,
20
+ attachments: wire.attachments,
21
+ fallbackText: textDelivery.fallbackText,
22
+ });
23
+ }
24
+ export async function editSlackContent(client, channel, ts, content) {
25
+ const wire = formatOutboundWire(content);
26
+ const blocks = [...wire.blocks];
27
+ const textDelivery = applyTextMrkdwnBlocks(wire.text, blocks);
28
+ const payloadBlocks = blocks.slice(0, SLACK_MAX_BLOCKS_PER_MESSAGE);
29
+ const updateOpts = {
30
+ channel,
31
+ ts,
32
+ text: textDelivery.fallbackText || ' ',
33
+ };
34
+ if (payloadBlocks.length > 0)
35
+ updateOpts.blocks = payloadBlocks;
36
+ await client.chat.update(updateOpts);
37
+ }
38
+ function applyTextMrkdwnBlocks(textContent, blocks) {
39
+ const trimmed = textContent.trim();
40
+ if (!trimmed)
41
+ return { fallbackText: '' };
42
+ const mrkdwn = markdownToMrkdwn(trimmed);
43
+ const sections = splitMrkdwnText(mrkdwn).map((chunk) => ({
44
+ type: 'section',
45
+ text: { type: 'mrkdwn', text: chunk },
46
+ }));
47
+ if (blocks.length > 0) {
48
+ blocks.unshift(...sections);
49
+ return { fallbackText: '' };
50
+ }
51
+ blocks.push(...sections);
52
+ return { fallbackText: mrkdwnToPlainFallback(mrkdwn) };
53
+ }
54
+ async function postSlackMessage(client, opts) {
55
+ const { channel, blocks, attachments, fallbackText } = opts;
56
+ let threadTs = opts.threadTs;
57
+ let firstTs = '';
58
+ if (blocks.length === 0) {
59
+ const result = await client.chat.postMessage({
60
+ channel,
61
+ text: fallbackText || 'Message',
62
+ ...(threadTs ? { thread_ts: threadTs } : {}),
63
+ ...(attachments.length > 0 ? { attachments } : {}),
64
+ });
65
+ return { ts: result.ts ?? '' };
66
+ }
67
+ for (let offset = 0; offset < blocks.length; offset += SLACK_MAX_BLOCKS_PER_MESSAGE) {
68
+ const chunk = blocks.slice(offset, offset + SLACK_MAX_BLOCKS_PER_MESSAGE);
69
+ const result = await client.chat.postMessage({
70
+ channel,
71
+ text: offset === 0 ? (fallbackText || ' ') : ' ',
72
+ blocks: chunk,
73
+ ...(threadTs ? { thread_ts: threadTs } : {}),
74
+ ...(offset === 0 && attachments.length > 0 ? { attachments } : {}),
75
+ });
76
+ const ts = result.ts ?? '';
77
+ if (!firstTs)
78
+ firstTs = ts;
79
+ if (!threadTs)
80
+ threadTs = ts;
81
+ }
82
+ return { ts: firstTs };
83
+ }
84
+ async function uploadFile(client, channel, file, threadTs, logger) {
85
+ if (!client.filesUploadV2)
86
+ return;
87
+ try {
88
+ let buffer = file.buffer;
89
+ if (!buffer && file.path) {
90
+ const { readFile } = await import('node:fs/promises');
91
+ buffer = await readFile(file.path);
92
+ }
93
+ if (!buffer && file.url) {
94
+ const res = await fetch(file.url);
95
+ if (!res.ok)
96
+ throw new Error(`fetch ${file.url}: ${res.status}`);
97
+ buffer = Buffer.from(await res.arrayBuffer());
98
+ }
99
+ if (!buffer)
100
+ return;
101
+ await client.filesUploadV2(threadTs
102
+ ? { channel_id: channel, file: buffer, filename: file.name ?? 'file', thread_ts: threadTs }
103
+ : { channel_id: channel, file: buffer, filename: file.name ?? 'file' });
104
+ }
105
+ catch (e) {
106
+ logger?.error('File upload failed:', e);
107
+ }
108
+ }
109
+ export { keyboardToBlockKitBlocks };
@@ -0,0 +1 @@
1
+ export declare function normalizeSlackReactionName(emoji: string): string;
@@ -24,4 +24,3 @@ export function normalizeSlackReactionName(emoji) {
24
24
  end--;
25
25
  return trimmed.slice(start, end);
26
26
  }
27
- //# sourceMappingURL=slack-reaction.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Slack response_url 即时 ephemeral 反馈(斜杠命令 / 按钮交互)
3
+ */
4
+ import type { Logger } from '@zhin.js/logger';
5
+ export declare function postSlackEphemeral(responseUrl: string, text: string, logger?: Logger): void;
@@ -14,4 +14,3 @@ export function postSlackEphemeral(responseUrl, text, logger) {
14
14
  logger?.debug(`Slack response_url failed: ${err instanceof Error ? err.message : String(err)}`);
15
15
  });
16
16
  }
17
- //# sourceMappingURL=slack-response-url.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Slack webhook HTTP: signature → parse → admit.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
6
+ import { type ResolvedSlackConfig, type SlackInteractionPayload, type SlackSlashCommand } from './protocol.js';
7
+ export interface SlackWebhookHandler {
8
+ readonly config: ResolvedSlackConfig;
9
+ handleEnvelope(body: unknown): void;
10
+ admitInteraction(payload: SlackInteractionPayload): void;
11
+ admitSlashCommand(cmd: SlackSlashCommand): void;
12
+ }
13
+ export declare function registerSlackWebhookRoutes(http: HttpHost, handler: SlackWebhookHandler): HttpRouteRegistration[];
14
+ export declare function handleSlackWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: SlackWebhookHandler): Promise<void>;