@zhin.js/adapter-slack 1.1.18 → 1.1.19

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/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: Record<string, unknown>[];
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: Record<string, unknown>[] = [];
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>): 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: Record<string, unknown>[] = [];
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',
@@ -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
- postMessage(opts: Record<string, unknown>): Promise<{ ts?: string }>;
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: unknown,
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: string,
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: Record<string, unknown> = {
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: Record<string, unknown>[],
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
- blocks: Record<string, unknown>[];
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: string,
148
- file: { buffer?: Buffer; url?: string; path?: string; name?: string },
149
- threadTs?: string,
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
- if (!client.filesUploadV2) throw new Error('Slack client does not support file uploads');
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 && file.path) {
154
- const { readFile } = await import('node:fs/promises');
155
- buffer = await readFile(file.path);
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 && file.url) {
158
- const res = await fetch(file.url);
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
- await client.filesUploadV2(
165
- threadTs
166
- ? { channel_id: channel, file: buffer, filename: file.name ?? 'file', thread_ts: threadTs }
167
- : { channel_id: channel, file: buffer, filename: file.name ?? 'file' },
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, { ...options, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true });
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');