@zhin.js/adapter-slack 1.1.18 → 1.1.20
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 +45 -0
- package/README.md +12 -5
- package/adapters/slack/index.js +3 -23
- package/adapters/slack/index.ts +2 -23
- package/agents/slack/skills/slack-messages/tools/edit_message/index.js +3 -3
- package/agents/slack/skills/slack-messages/tools/edit_message/index.ts +2 -2
- package/commands/slack/endpoint/definition.js +0 -2
- package/commands/slack/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +16 -0
- package/lib/adapter.js +54 -0
- package/lib/client.d.ts +3 -7
- package/lib/client.js +1 -2
- package/lib/console-spec.js +6 -0
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +12 -115
- package/lib/endpoint.js +237 -183
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +8 -2
- package/lib/slack-outbound.d.ts +16 -12
- package/lib/slack-outbound.js +28 -19
- package/lib/web-client.d.ts +1 -0
- package/lib/web-client.js +4 -1
- package/package.json +18 -18
- package/plugin.js +1 -4
- package/src/adapter.ts +38 -0
- package/src/client.ts +2 -10
- package/src/console-spec.ts +6 -0
- package/src/console.ts +2 -6
- package/src/endpoint.ts +239 -226
- package/src/index.ts +1 -1
- package/src/protocol.ts +7 -4
- package/src/slack-outbound.ts +50 -37
- package/src/web-client.ts +5 -1
- package/lib/slack-runtime-state.d.ts +0 -1
- package/lib/slack-runtime-state.js +0 -6
- package/src/slack-runtime-state.ts +0 -7
package/src/index.ts
CHANGED
|
@@ -37,7 +37,6 @@ export {
|
|
|
37
37
|
} from './webhook.js';
|
|
38
38
|
|
|
39
39
|
export {
|
|
40
|
-
slackClient,
|
|
41
40
|
type SlackClientEventMap,
|
|
42
41
|
type SlackUserInfo,
|
|
43
42
|
} from './client.js';
|
|
@@ -57,3 +56,4 @@ export {
|
|
|
57
56
|
shouldDropSlackInboundMessage,
|
|
58
57
|
} from './slack-inbound-filter.js';
|
|
59
58
|
export { editSlackContent, sendSlackContent } from './slack-outbound.js';
|
|
59
|
+
export { SlackAdapter } from './adapter.js';
|
package/src/protocol.ts
CHANGED
|
@@ -121,6 +121,9 @@ export interface SlackWireSegment {
|
|
|
121
121
|
readonly data?: Record<string, unknown>;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/** Every block emitted by the encoder has Slack's required native block discriminator. */
|
|
125
|
+
export interface SlackBlock { readonly type: string; readonly block_id?: string; readonly [key: string]: unknown }
|
|
126
|
+
|
|
124
127
|
export function resolveSlackConfig(config: SlackEndpointConfig): ResolvedSlackConfig {
|
|
125
128
|
if (config.webApiProxy && (!Number.isInteger(config.webApiProxy.port) || config.webApiProxy.port < 1 || config.webApiProxy.port > 65535)) throw new TypeError('Invalid Slack webApiProxy port');
|
|
126
129
|
if (config.streamProxy && (!Number.isInteger(config.streamProxy.port) || config.streamProxy.port < 1 || config.streamProxy.port > 65535
|
|
@@ -225,7 +228,7 @@ export function formatSlashContent(cmd: SlackSlashCommand): string {
|
|
|
225
228
|
export function formatOutboundWire(payload: unknown): {
|
|
226
229
|
replyTo?: string;
|
|
227
230
|
text: string;
|
|
228
|
-
blocks:
|
|
231
|
+
blocks: SlackBlock[];
|
|
229
232
|
attachments: Record<string, unknown>[];
|
|
230
233
|
files: Array<{ buffer?: Buffer; url?: string; path?: string; name?: string }>;
|
|
231
234
|
} {
|
|
@@ -249,7 +252,7 @@ export function formatOutboundWire(payload: unknown): {
|
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
let text = '';
|
|
252
|
-
const blocks:
|
|
255
|
+
const blocks: SlackBlock[] = [];
|
|
253
256
|
const attachments: Record<string, unknown>[] = [];
|
|
254
257
|
const files: Array<{ buffer?: Buffer; url?: string; path?: string; name?: string }> = [];
|
|
255
258
|
let replyTo: string | undefined;
|
|
@@ -328,11 +331,11 @@ export function formatOutboundWire(payload: unknown): {
|
|
|
328
331
|
return { text, blocks, attachments, files, ...(replyTo ? { replyTo } : {}) };
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
export function keyboardToBlockKitBlocks(data: Record<string, unknown>):
|
|
334
|
+
export function keyboardToBlockKitBlocks(data: Record<string, unknown>): SlackBlock[] {
|
|
332
335
|
const rows = data.rows as Array<Array<Record<string, unknown>>> | undefined;
|
|
333
336
|
if (!rows?.length) return [];
|
|
334
337
|
|
|
335
|
-
const blocks:
|
|
338
|
+
const blocks: SlackBlock[] = [];
|
|
336
339
|
for (const row of rows) {
|
|
337
340
|
const elements = row.slice(0, 5).map((btn, index) => ({
|
|
338
341
|
type: 'button',
|
package/src/slack-outbound.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slack outbound — chat.postMessage + Block Kit + files.uploadV2
|
|
3
3
|
*/
|
|
4
|
+
import { createEndpointOperation, type EndpointOperation } from 'zhin.js/adapter';
|
|
4
5
|
import type { Logger } from '@zhin.js/logger';
|
|
6
|
+
import type { WebClient } from '@slack/web-api';
|
|
5
7
|
import { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
|
|
6
|
-
import { formatOutboundWire, keyboardToBlockKitBlocks } from './protocol.js';
|
|
8
|
+
import { formatOutboundWire, keyboardToBlockKitBlocks, type SlackBlock } from './protocol.js';
|
|
7
9
|
|
|
8
10
|
export interface SlackOutboundResult {
|
|
9
11
|
ts: string;
|
|
@@ -15,21 +17,28 @@ export interface SlackSendOptions {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface SlackChatClient {
|
|
18
|
-
chat:
|
|
19
|
-
|
|
20
|
-
update(opts: Record<string, unknown>): Promise<unknown>;
|
|
21
|
-
};
|
|
22
|
-
filesUploadV2?(opts: Record<string, unknown>): Promise<unknown>;
|
|
20
|
+
chat: Pick<WebClient['chat'], 'postMessage' | 'update'>;
|
|
21
|
+
filesUploadV2?: WebClient['filesUploadV2'];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
const SLACK_MAX_BLOCKS_PER_MESSAGE = 48;
|
|
26
25
|
|
|
26
|
+
export interface SlackContentRequest extends SlackSendOptions {
|
|
27
|
+
readonly content: unknown;
|
|
28
|
+
readonly logger: Logger;
|
|
29
|
+
readonly signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
export interface SlackEditRequest {
|
|
32
|
+
readonly channel: string;
|
|
33
|
+
readonly ts: string;
|
|
34
|
+
readonly content: unknown;
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
}
|
|
27
37
|
export async function sendSlackContent(
|
|
28
38
|
client: SlackChatClient,
|
|
29
|
-
content:
|
|
30
|
-
opts: SlackSendOptions,
|
|
31
|
-
logger: Logger,
|
|
39
|
+
{ content, logger, signal, ...opts }: SlackContentRequest,
|
|
32
40
|
): Promise<SlackOutboundResult> {
|
|
41
|
+
const operation = createEndpointOperation({ signal });
|
|
33
42
|
const wire = formatOutboundWire(content);
|
|
34
43
|
const threadTs = opts.threadTs ?? wire.replyTo;
|
|
35
44
|
const blocks = [...wire.blocks];
|
|
@@ -37,7 +46,7 @@ export async function sendSlackContent(
|
|
|
37
46
|
|
|
38
47
|
for (const pf of wire.files) {
|
|
39
48
|
try {
|
|
40
|
-
await uploadFile(client, opts.channel, pf, threadTs);
|
|
49
|
+
await uploadFile(client, { channel: opts.channel, file: pf, threadTs, operation });
|
|
41
50
|
} catch (e) {
|
|
42
51
|
logger.error('Failed to upload file:', e);
|
|
43
52
|
throw e;
|
|
@@ -47,6 +56,7 @@ export async function sendSlackContent(
|
|
|
47
56
|
return postSlackMessage(client, {
|
|
48
57
|
channel: opts.channel,
|
|
49
58
|
threadTs,
|
|
59
|
+
operation,
|
|
50
60
|
blocks,
|
|
51
61
|
attachments: wire.attachments,
|
|
52
62
|
fallbackText: textDelivery.fallbackText,
|
|
@@ -55,27 +65,25 @@ export async function sendSlackContent(
|
|
|
55
65
|
|
|
56
66
|
export async function editSlackContent(
|
|
57
67
|
client: SlackChatClient,
|
|
58
|
-
channel:
|
|
59
|
-
ts: string,
|
|
60
|
-
content: unknown,
|
|
68
|
+
{ channel, ts, content, signal }: SlackEditRequest,
|
|
61
69
|
): Promise<void> {
|
|
62
70
|
const wire = formatOutboundWire(content);
|
|
63
71
|
const blocks = [...wire.blocks];
|
|
64
72
|
const textDelivery = applyTextMrkdwnBlocks(wire.text, blocks);
|
|
65
73
|
const payloadBlocks = blocks.slice(0, SLACK_MAX_BLOCKS_PER_MESSAGE);
|
|
66
74
|
|
|
67
|
-
const updateOpts:
|
|
75
|
+
const updateOpts: { channel: string; ts: string; text: string; blocks?: SlackBlock[] } = {
|
|
68
76
|
channel,
|
|
69
77
|
ts,
|
|
70
78
|
text: textDelivery.fallbackText || ' ',
|
|
71
79
|
};
|
|
72
80
|
if (payloadBlocks.length > 0) updateOpts.blocks = payloadBlocks;
|
|
73
|
-
await client.chat.update(updateOpts);
|
|
81
|
+
await createEndpointOperation({ signal }).run(() => client.chat.update(updateOpts));
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
function applyTextMrkdwnBlocks(
|
|
77
85
|
textContent: string,
|
|
78
|
-
blocks:
|
|
86
|
+
blocks: SlackBlock[],
|
|
79
87
|
): { fallbackText: string } {
|
|
80
88
|
const trimmed = textContent.trim();
|
|
81
89
|
if (!trimmed) return { fallbackText: '' };
|
|
@@ -99,34 +107,35 @@ async function postSlackMessage(
|
|
|
99
107
|
opts: {
|
|
100
108
|
channel: string;
|
|
101
109
|
threadTs?: string;
|
|
102
|
-
|
|
110
|
+
operation: EndpointOperation;
|
|
111
|
+
blocks: SlackBlock[];
|
|
103
112
|
attachments: Record<string, unknown>[];
|
|
104
113
|
fallbackText: string;
|
|
105
114
|
},
|
|
106
115
|
): Promise<SlackOutboundResult> {
|
|
107
|
-
const { channel, blocks, attachments, fallbackText } = opts;
|
|
116
|
+
const { channel, blocks, attachments, fallbackText, operation } = opts;
|
|
108
117
|
let threadTs = opts.threadTs;
|
|
109
118
|
let firstTs = '';
|
|
110
119
|
|
|
111
120
|
if (blocks.length === 0) {
|
|
112
|
-
const result = await client.chat.postMessage({
|
|
121
|
+
const result = await operation.run(() => client.chat.postMessage({
|
|
113
122
|
channel,
|
|
114
123
|
text: fallbackText || 'Message',
|
|
115
124
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
116
125
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
117
|
-
});
|
|
126
|
+
}));
|
|
118
127
|
return { ts: requireMessageTimestamp(result) };
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
for (let offset = 0; offset < blocks.length; offset += SLACK_MAX_BLOCKS_PER_MESSAGE) {
|
|
122
131
|
const chunk = blocks.slice(offset, offset + SLACK_MAX_BLOCKS_PER_MESSAGE);
|
|
123
|
-
const result = await client.chat.postMessage({
|
|
132
|
+
const result = await operation.run(() => client.chat.postMessage({
|
|
124
133
|
channel,
|
|
125
134
|
text: offset === 0 ? (fallbackText || ' ') : ' ',
|
|
126
135
|
blocks: chunk,
|
|
127
136
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
128
137
|
...(offset === 0 && attachments.length > 0 ? { attachments } : {}),
|
|
129
|
-
});
|
|
138
|
+
}));
|
|
130
139
|
const ts = requireMessageTimestamp(result);
|
|
131
140
|
if (!firstTs) firstTs = ts;
|
|
132
141
|
if (!threadTs) threadTs = ts;
|
|
@@ -144,28 +153,32 @@ function requireMessageTimestamp(result: { ts?: string } | null | undefined): st
|
|
|
144
153
|
|
|
145
154
|
async function uploadFile(
|
|
146
155
|
client: SlackChatClient,
|
|
147
|
-
channel:
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
{ channel, file, threadTs, operation }: {
|
|
157
|
+
readonly channel: string;
|
|
158
|
+
readonly file: { buffer?: Buffer; url?: string; path?: string; name?: string };
|
|
159
|
+
readonly threadTs?: string;
|
|
160
|
+
readonly operation: EndpointOperation;
|
|
161
|
+
},
|
|
150
162
|
): Promise<void> {
|
|
151
|
-
|
|
163
|
+
const upload = client.filesUploadV2;
|
|
164
|
+
if (!upload) throw new Error('Slack client does not support file uploads');
|
|
165
|
+
const { path, url } = file;
|
|
152
166
|
let buffer = file.buffer;
|
|
153
|
-
if (!buffer &&
|
|
154
|
-
const { readFile } = await import('node:fs/promises');
|
|
155
|
-
buffer = await readFile(
|
|
167
|
+
if (!buffer && path) {
|
|
168
|
+
const { readFile } = await operation.prepare(() => import('node:fs/promises'));
|
|
169
|
+
buffer = await operation.prepare(() => readFile(path));
|
|
156
170
|
}
|
|
157
|
-
if (!buffer &&
|
|
158
|
-
const res = await fetch(
|
|
171
|
+
if (!buffer && url) {
|
|
172
|
+
const res = await operation.prepare(() => fetch(url, { signal: operation.signal }));
|
|
159
173
|
if (!res.ok) throw new Error(`fetch ${file.url}: ${res.status}`);
|
|
160
|
-
buffer = Buffer.from(await res.arrayBuffer());
|
|
174
|
+
buffer = Buffer.from(await operation.prepare(() => res.arrayBuffer()));
|
|
161
175
|
}
|
|
162
176
|
if (!buffer) throw new Error('Slack file contains no uploadable content');
|
|
163
177
|
|
|
164
|
-
|
|
165
|
-
threadTs
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
178
|
+
const options: Parameters<typeof upload>[0] = threadTs
|
|
179
|
+
? { channel_id: channel, file: buffer, filename: file.name ?? 'file', thread_ts: threadTs }
|
|
180
|
+
: { channel_id: channel, file: buffer, filename: file.name ?? 'file' };
|
|
181
|
+
await operation.run(() => upload.call(client, options));
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
export { keyboardToBlockKitBlocks };
|
package/src/web-client.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
3
3
|
|
|
4
4
|
/** A non-idempotent outbound API call gets exactly one transport attempt. */
|
|
5
5
|
export function createSlackWebClient(token: string, options: WebClientOptions = {}): WebClient {
|
|
6
|
-
return new WebClient(token,
|
|
6
|
+
return new WebClient(token, slackWebClientOptions(options));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function slackWebClientOptions(options: WebClientOptions = {}): WebClientOptions {
|
|
10
|
+
return { ...options, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true };
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export function slackDeliveryError(error: unknown): EndpointDeliveryError {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const slackRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slack 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `slack endpoint` 命令共享(同一 owner generation)。
|
|
4
|
-
*/
|
|
5
|
-
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
|
-
export const slackRuntimeStateToken = defineEndpointRuntimeStateToken('slack');
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slack 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `slack endpoint` 命令共享(同一 owner generation)。
|
|
4
|
-
*/
|
|
5
|
-
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
|
-
|
|
7
|
-
export const slackRuntimeStateToken = defineEndpointRuntimeStateToken('slack');
|