@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/lib/protocol.d.ts CHANGED
@@ -109,6 +109,12 @@ export interface SlackWireSegment {
109
109
  readonly type: string;
110
110
  readonly data?: Record<string, unknown>;
111
111
  }
112
+ /** Every block emitted by the encoder has Slack's required native block discriminator. */
113
+ export interface SlackBlock {
114
+ readonly type: string;
115
+ readonly block_id?: string;
116
+ readonly [key: string]: unknown;
117
+ }
112
118
  export declare function resolveSlackConfig(config: SlackEndpointConfig): ResolvedSlackConfig;
113
119
  export declare function normalizeWebhookPath(path: string): string;
114
120
  export declare function resolveSlackChannelType(event: Pick<SlackEvent, 'channel_type'>): 'private' | 'group';
@@ -133,7 +139,7 @@ export declare function formatSlashContent(cmd: SlackSlashCommand): string;
133
139
  export declare function formatOutboundWire(payload: unknown): {
134
140
  replyTo?: string;
135
141
  text: string;
136
- blocks: Record<string, unknown>[];
142
+ blocks: SlackBlock[];
137
143
  attachments: Record<string, unknown>[];
138
144
  files: Array<{
139
145
  buffer?: Buffer;
@@ -142,7 +148,7 @@ export declare function formatOutboundWire(payload: unknown): {
142
148
  name?: string;
143
149
  }>;
144
150
  };
145
- export declare function keyboardToBlockKitBlocks(data: Record<string, unknown>): Record<string, unknown>[];
151
+ export declare function keyboardToBlockKitBlocks(data: Record<string, unknown>): SlackBlock[];
146
152
  export declare function verifySlackSignature(signingSecret: string, rawBody: string, timestamp: string, signature: string): boolean;
147
153
  export declare function readTextBody(request: IncomingMessage, options?: {
148
154
  readonly limit?: number;
@@ -1,7 +1,5 @@
1
- /**
2
- * Slack outbound — chat.postMessage + Block Kit + files.uploadV2
3
- */
4
1
  import type { Logger } from '@zhin.js/logger';
2
+ import type { WebClient } from '@slack/web-api';
5
3
  import { keyboardToBlockKitBlocks } from './protocol.js';
6
4
  export interface SlackOutboundResult {
7
5
  ts: string;
@@ -11,14 +9,20 @@ export interface SlackSendOptions {
11
9
  threadTs?: string;
12
10
  }
13
11
  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>;
12
+ chat: Pick<WebClient['chat'], 'postMessage' | 'update'>;
13
+ filesUploadV2?: WebClient['filesUploadV2'];
21
14
  }
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>;
15
+ export interface SlackContentRequest extends SlackSendOptions {
16
+ readonly content: unknown;
17
+ readonly logger: Logger;
18
+ readonly signal?: AbortSignal;
19
+ }
20
+ export interface SlackEditRequest {
21
+ readonly channel: string;
22
+ readonly ts: string;
23
+ readonly content: unknown;
24
+ readonly signal?: AbortSignal;
25
+ }
26
+ export declare function sendSlackContent(client: SlackChatClient, { content, logger, signal, ...opts }: SlackContentRequest): Promise<SlackOutboundResult>;
27
+ export declare function editSlackContent(client: SlackChatClient, { channel, ts, content, signal }: SlackEditRequest): Promise<void>;
24
28
  export { keyboardToBlockKitBlocks };
@@ -1,14 +1,19 @@
1
+ /**
2
+ * Slack outbound — chat.postMessage + Block Kit + files.uploadV2
3
+ */
4
+ import { createEndpointOperation } from 'zhin.js/adapter';
1
5
  import { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
2
6
  import { formatOutboundWire, keyboardToBlockKitBlocks } from './protocol.js';
3
7
  const SLACK_MAX_BLOCKS_PER_MESSAGE = 48;
4
- export async function sendSlackContent(client, content, opts, logger) {
8
+ export async function sendSlackContent(client, { content, logger, signal, ...opts }) {
9
+ const operation = createEndpointOperation({ signal });
5
10
  const wire = formatOutboundWire(content);
6
11
  const threadTs = opts.threadTs ?? wire.replyTo;
7
12
  const blocks = [...wire.blocks];
8
13
  const textDelivery = applyTextMrkdwnBlocks(wire.text, blocks);
9
14
  for (const pf of wire.files) {
10
15
  try {
11
- await uploadFile(client, opts.channel, pf, threadTs);
16
+ await uploadFile(client, { channel: opts.channel, file: pf, threadTs, operation });
12
17
  }
13
18
  catch (e) {
14
19
  logger.error('Failed to upload file:', e);
@@ -18,12 +23,13 @@ export async function sendSlackContent(client, content, opts, logger) {
18
23
  return postSlackMessage(client, {
19
24
  channel: opts.channel,
20
25
  threadTs,
26
+ operation,
21
27
  blocks,
22
28
  attachments: wire.attachments,
23
29
  fallbackText: textDelivery.fallbackText,
24
30
  });
25
31
  }
26
- export async function editSlackContent(client, channel, ts, content) {
32
+ export async function editSlackContent(client, { channel, ts, content, signal }) {
27
33
  const wire = formatOutboundWire(content);
28
34
  const blocks = [...wire.blocks];
29
35
  const textDelivery = applyTextMrkdwnBlocks(wire.text, blocks);
@@ -35,7 +41,7 @@ export async function editSlackContent(client, channel, ts, content) {
35
41
  };
36
42
  if (payloadBlocks.length > 0)
37
43
  updateOpts.blocks = payloadBlocks;
38
- await client.chat.update(updateOpts);
44
+ await createEndpointOperation({ signal }).run(() => client.chat.update(updateOpts));
39
45
  }
40
46
  function applyTextMrkdwnBlocks(textContent, blocks) {
41
47
  const trimmed = textContent.trim();
@@ -54,27 +60,27 @@ function applyTextMrkdwnBlocks(textContent, blocks) {
54
60
  return { fallbackText: mrkdwnToPlainFallback(mrkdwn) };
55
61
  }
56
62
  async function postSlackMessage(client, opts) {
57
- const { channel, blocks, attachments, fallbackText } = opts;
63
+ const { channel, blocks, attachments, fallbackText, operation } = opts;
58
64
  let threadTs = opts.threadTs;
59
65
  let firstTs = '';
60
66
  if (blocks.length === 0) {
61
- const result = await client.chat.postMessage({
67
+ const result = await operation.run(() => client.chat.postMessage({
62
68
  channel,
63
69
  text: fallbackText || 'Message',
64
70
  ...(threadTs ? { thread_ts: threadTs } : {}),
65
71
  ...(attachments.length > 0 ? { attachments } : {}),
66
- });
72
+ }));
67
73
  return { ts: requireMessageTimestamp(result) };
68
74
  }
69
75
  for (let offset = 0; offset < blocks.length; offset += SLACK_MAX_BLOCKS_PER_MESSAGE) {
70
76
  const chunk = blocks.slice(offset, offset + SLACK_MAX_BLOCKS_PER_MESSAGE);
71
- const result = await client.chat.postMessage({
77
+ const result = await operation.run(() => client.chat.postMessage({
72
78
  channel,
73
79
  text: offset === 0 ? (fallbackText || ' ') : ' ',
74
80
  blocks: chunk,
75
81
  ...(threadTs ? { thread_ts: threadTs } : {}),
76
82
  ...(offset === 0 && attachments.length > 0 ? { attachments } : {}),
77
- });
83
+ }));
78
84
  const ts = requireMessageTimestamp(result);
79
85
  if (!firstTs)
80
86
  firstTs = ts;
@@ -89,24 +95,27 @@ function requireMessageTimestamp(result) {
89
95
  }
90
96
  return result.ts;
91
97
  }
92
- async function uploadFile(client, channel, file, threadTs) {
93
- if (!client.filesUploadV2)
98
+ async function uploadFile(client, { channel, file, threadTs, operation }) {
99
+ const upload = client.filesUploadV2;
100
+ if (!upload)
94
101
  throw new Error('Slack client does not support file uploads');
102
+ const { path, url } = file;
95
103
  let buffer = file.buffer;
96
- if (!buffer && file.path) {
97
- const { readFile } = await import('node:fs/promises');
98
- buffer = await readFile(file.path);
104
+ if (!buffer && path) {
105
+ const { readFile } = await operation.prepare(() => import('node:fs/promises'));
106
+ buffer = await operation.prepare(() => readFile(path));
99
107
  }
100
- if (!buffer && file.url) {
101
- const res = await fetch(file.url);
108
+ if (!buffer && url) {
109
+ const res = await operation.prepare(() => fetch(url, { signal: operation.signal }));
102
110
  if (!res.ok)
103
111
  throw new Error(`fetch ${file.url}: ${res.status}`);
104
- buffer = Buffer.from(await res.arrayBuffer());
112
+ buffer = Buffer.from(await operation.prepare(() => res.arrayBuffer()));
105
113
  }
106
114
  if (!buffer)
107
115
  throw new Error('Slack file contains no uploadable content');
108
- await client.filesUploadV2(threadTs
116
+ const options = threadTs
109
117
  ? { channel_id: channel, file: buffer, filename: file.name ?? 'file', thread_ts: threadTs }
110
- : { channel_id: channel, file: buffer, filename: file.name ?? 'file' });
118
+ : { channel_id: channel, file: buffer, filename: file.name ?? 'file' };
119
+ await operation.run(() => upload.call(client, options));
111
120
  }
112
121
  export { keyboardToBlockKitBlocks };
@@ -2,4 +2,5 @@ import { WebClient, type WebClientOptions } from '@slack/web-api';
2
2
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
3
3
  /** A non-idempotent outbound API call gets exactly one transport attempt. */
4
4
  export declare function createSlackWebClient(token: string, options?: WebClientOptions): WebClient;
5
+ export declare function slackWebClientOptions(options?: WebClientOptions): WebClientOptions;
5
6
  export declare function slackDeliveryError(error: unknown): EndpointDeliveryError;
package/lib/web-client.js CHANGED
@@ -2,7 +2,10 @@ import { WebClient, ErrorCode } from '@slack/web-api';
2
2
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
3
3
  /** A non-idempotent outbound API call gets exactly one transport attempt. */
4
4
  export function createSlackWebClient(token, options = {}) {
5
- return new WebClient(token, { ...options, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true });
5
+ return new WebClient(token, slackWebClientOptions(options));
6
+ }
7
+ export function slackWebClientOptions(options = {}) {
8
+ return { ...options, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true };
6
9
  }
7
10
  export function slackDeliveryError(error) {
8
11
  if (error instanceof EndpointDeliveryError)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-slack",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Zhin.js Slack adapter for Plugin Runtime (Socket Mode / HTTP Events)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,27 +33,27 @@
33
33
  "dependencies": {
34
34
  "@slack/socket-mode": "^2.0.7",
35
35
  "@slack/web-api": "^7.19.0",
36
- "@zhin.js/adapter": "1.1.25",
37
- "@zhin.js/agent-feature": "1.1.10",
38
- "@zhin.js/client": "1.1.21",
36
+ "@zhin.js/adapter": "1.1.26",
37
+ "@zhin.js/agent-feature": "1.1.11",
38
+ "@zhin.js/client": "1.1.22",
39
39
  "@zhin.js/console-contract": "1.1.3",
40
- "@zhin.js/core": "1.1.53",
41
- "@zhin.js/feature-kit": "1.1.9",
42
- "@zhin.js/host-http": "1.1.11",
40
+ "@zhin.js/core": "1.1.54",
41
+ "@zhin.js/feature-kit": "1.1.10",
42
+ "@zhin.js/host-http": "1.1.12",
43
43
  "@zhin.js/im-contract": "1.1.9",
44
44
  "@zhin.js/logger": "1.1.2",
45
- "@zhin.js/page": "1.1.10",
46
- "@zhin.js/skill": "1.1.10",
47
- "@zhin.js/tool": "1.1.12"
45
+ "@zhin.js/page": "1.1.12",
46
+ "@zhin.js/skill": "1.1.11",
47
+ "@zhin.js/tool": "1.1.13"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "zod": "^4.0.0",
51
- "@zhin.js/adapter": "^1.1.25",
52
- "@zhin.js/command": "^1.1.11",
53
- "@zhin.js/core": "^1.1.53",
54
- "@zhin.js/host-http": "^1.1.11",
55
- "@zhin.js/permission": "^1.1.9",
56
- "zhin.js": "^1.1.18"
51
+ "@zhin.js/adapter": "^1.1.26",
52
+ "@zhin.js/command": "^1.1.12",
53
+ "@zhin.js/core": "^1.1.54",
54
+ "@zhin.js/host-http": "^1.1.12",
55
+ "@zhin.js/permission": "^1.1.10",
56
+ "zhin.js": "^1.1.19"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@zhin.js/command": {
@@ -71,7 +71,7 @@
71
71
  "typescript": "^6.0.3",
72
72
  "vitest": "^4.1.10",
73
73
  "zod": "^4.4.3",
74
- "zhin.js": "1.1.18"
74
+ "zhin.js": "1.1.19"
75
75
  },
76
76
  "files": [
77
77
  "adapters",
@@ -103,7 +103,7 @@
103
103
  "features": [
104
104
  {
105
105
  "package": "@zhin.js/adapter",
106
- "api": "^1.0.0"
106
+ "api": "^2.0.0"
107
107
  },
108
108
  {
109
109
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
3
  import { permissionHostToken } from '@zhin.js/permission';
5
4
  import { checkSlackPlatformPermit } from "./lib/platform-permit.js";
6
- import { slackRuntimeStateToken } from "./lib/slack-runtime-state.js";
7
5
  import { createSlackConsoleRpc } from "./lib/console.js";
8
6
  export default definePlugin({
9
7
  name: 'slack',
@@ -11,8 +9,7 @@ export default definePlugin({
11
9
  displayName: 'Slack Adapter',
12
10
  },
13
11
  setup(context) {
14
- context.resources.provide(slackRuntimeStateToken, createEndpointRuntimeState());
15
- context.resources.provide(pluginConsoleRpcToken, createSlackConsoleRpc(context.resources, () => context.config.get()));
12
+ context.resources.provide(pluginConsoleRpcToken, createSlackConsoleRpc(() => context.config.get()));
16
13
  if (context.resources.has(permissionHostToken)) {
17
14
  const host = context.resources.use(permissionHostToken);
18
15
  return host.registerPlatform('slack', checkSlackPlatformPermit);
package/src/adapter.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { httpHostToken } from '@zhin.js/host-http';
3
+ import { WebClient, type WebClientOptions } from '@slack/web-api';
4
+ import { SlackEndpoint } from './endpoint.js';
5
+ import { slackWebClientOptions } from './web-client.js';
6
+ import { createSlackWebApiAgent } from './web-api-proxy.js';
7
+ import { resolveSlackConfig, type SlackEndpointConfig } from './protocol.js';
8
+ import type { SlackClientEventMap } from './client.js';
9
+
10
+ export class SlackAdapter extends Adapter<SlackEndpoint, SlackEndpointConfig, SlackClientEventMap> {
11
+ constructor(private readonly context: AdapterContext<SlackEndpointConfig>, private readonly webOptions: WebClientOptions = {}) { super(); }
12
+ override get configurationIdentityFields(): readonly string[] { return ['id']; }
13
+ override get configurationIdentityProbeFields(): readonly string[] { return ['token', 'appToken', 'signingSecret']; }
14
+ override get configurationUpdateFields(): readonly string[] { return ['token', 'appToken', 'signingSecret', 'socketMode',
15
+ 'webhookPath', 'clientPingTimeout', 'webApiProxy', 'webApiProxy.port', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName']; }
16
+ protected override validateConfiguration(config: SlackEndpointConfig): void { resolveSlackConfig(config); }
17
+ protected override async identifyConfiguration(input: SlackEndpointConfig, signal: AbortSignal, changedFields: readonly string[]): Promise<string | undefined> {
18
+ // Neither the app-level token nor the callback signing secret has a readonly identity API.
19
+ if (changedFields.some(field => field !== 'token')) return undefined;
20
+ const config = resolveSlackConfig(input);
21
+ const agent = config.webApiProxy ? createSlackWebApiAgent(config.webApiProxy) : undefined;
22
+ const client = new WebClient(config.token, slackWebClientOptions({ ...this.webOptions, ...(agent ? { agent } : {}),
23
+ requestInterceptor: request => { request.signal = signal; return request; } }));
24
+ try {
25
+ const me = await client.auth.test();
26
+ if (typeof me.team_id !== 'string' || !me.team_id.trim()
27
+ || typeof me.user_id !== 'string' || !me.user_id.trim()) throw new Error('Slack identity response is incomplete');
28
+ return JSON.stringify(['slack', me.team_id, me.user_id]);
29
+ } finally { agent?.destroy(); }
30
+ }
31
+ protected createEndpoint(input: SlackEndpointConfig, execution: EndpointCreationContext): SlackEndpoint {
32
+ const config = resolveSlackConfig(input);
33
+ const webApiAgent = config.webApiProxy ? createSlackWebApiAgent(config.webApiProxy) : undefined;
34
+ try { return new SlackEndpoint({ id: execution.canonicalId, config, webApiAgent, webOptions: this.webOptions,
35
+ ...(config.mode === 'http' ? { http: this.context.use(httpHostToken) } : {}) }); }
36
+ catch (error) { webApiAgent?.destroy(); throw error; }
37
+ }
38
+ }
package/src/client.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { defineEndpointClient } from 'zhin.js/adapter';
2
- import type { SlackWebClientLike } from './endpoint.js';
1
+ import type { SlackAdapter } from './adapter.js';
3
2
  import type { SlackEvent } from './protocol.js';
4
3
 
5
4
  /** Slack users.info response fields projected by the bundled tools. */
@@ -19,12 +18,5 @@ export interface SlackUserInfo {
19
18
  export type SlackClientEventMap = Record<string, SlackEvent>;
20
19
 
21
20
  declare module '@zhin.js/feature-kit' {
22
- interface AdapterClientRegistry {
23
- readonly slack: {
24
- readonly client: SlackWebClientLike;
25
- readonly events: SlackClientEventMap;
26
- };
27
- }
21
+ interface AdapterRegistry { readonly slack: SlackAdapter; }
28
22
  }
29
-
30
- export const slackClient = defineEndpointClient<SlackWebClientLike, SlackClientEventMap>('slack');
@@ -10,6 +10,12 @@ export const slackConsoleSpec = {
10
10
  { key: 'token', required: true, env: true, description: 'Bot User OAuth Token(xoxb-...)' },
11
11
  { key: 'signingSecret', env: true, description: '签名密钥(HTTP Events,socketMode: false 必填)', descriptionI18n: { en: 'Signing secret (required for HTTP Events)' } },
12
12
  { key: 'appToken', env: true, description: 'App-Level Token(xapp-...,Socket Mode)', descriptionI18n: { en: 'App-level token (xapp-..., Socket Mode)' } },
13
+ { key: 'socketMode', type: 'boolean', description: 'Socket Mode; false selects HTTP Events' },
14
+ { key: 'webhookPath', description: 'HTTP Events route' },
15
+ { key: 'clientPingTimeout', type: 'number', description: 'SDK client ping timeout in milliseconds' },
16
+ { key: 'webApiProxy.port', type: 'number', description: 'Configured Web API loopback proxy port' },
17
+ { key: 'streamProxy.port', type: 'number', description: 'Configured Socket Mode loopback proxy port' },
18
+ { key: 'streamProxy.serverName', description: 'Expected Socket Mode TLS hostname' },
13
19
  ] as readonly AdapterConsoleField[],
14
20
  docsUrl: 'https://zhin.js.org/adapters/slack',
15
21
  readPermission: 'slack:read',
package/src/console.ts CHANGED
@@ -3,19 +3,15 @@
3
3
  *
4
4
  * 页面绑定 `pages/index`,读取 / 写入分别要求 `<adapter>:read` / `<adapter>:write`。
5
5
  */
6
- import { createAdapterConsoleRpc, type AdapterConsoleResources } from 'zhin.js/adapter';
6
+ import { createAdapterConsoleRpc } from 'zhin.js/adapter';
7
7
  import type { PluginConsoleRpc } from 'zhin.js';
8
- import { slackRuntimeStateToken } from './slack-runtime-state.js';
9
8
  import { slackConsoleSpec } from './console-spec.js';
10
9
 
11
10
  export function createSlackConsoleRpc(
12
- resources: AdapterConsoleResources,
13
11
  getConfig: () => unknown,
14
12
  ): PluginConsoleRpc {
15
- const state = resources.use(slackRuntimeStateToken);
16
13
  return createAdapterConsoleRpc({
17
14
  ...slackConsoleSpec,
18
- running: () => state.endpoints.values(),
19
15
  getConfig,
20
- }, resources);
16
+ });
21
17
  }