@zhin.js/adapter-slack 5.0.0 → 5.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - cdf64e7: 多方向审计修复批(8 面 30+ bug):
8
+
9
+ - **安全**:钉钉 webhook 验签绕过修复(缺 timestamp/sign 一律 403 + ±1h 防重放);exec-policy fail-closed(换行/`$(`/反引号拒绝、管道逐段过白名单、env dump 复合拆段);wecom 验签改 timingSafeEqual;config:set 拒绝 `__proto__` 等魔术键、host 键优先防覆写。
10
+ - **P0 功能**:`zhin packages` scoped 包名解析为空导致 rm -rf 风险;命令数字参数解析失败炸断消息链路(dispatch 捕获 continue);TaskQueue 监听器首个事件自摘除导致 assistant 队列挂死(+超时后完成不覆盖终态);DatabaseHost 跨世代共享崩溃(define 幂等 + stop 改进程级);rss 按不存在 id 列删除改业务键。
11
+ - **Runtime/HMR**:native watcher 过滤忽略目录(lib/.zhin 不再触发重载风暴);host 段配置 patch(http.port 等)存在 installResources 时全量重建;capability 目录非 entry 支持文件升级进程重启;documentTransaction 失败回滚。
12
+ - **CLI 写侧**:config/setup 对 toml 静默假成功改统一 config-file(不支持格式报错);doctor/onboard 默认配置改新形态 plugins 映射;schedule add 改 --prompt;migrate engines/中文模板误伤/覆盖前备份。
13
+ - **适配器生命周期**:napcat/milky start 失败竞态与僵尸连接、心跳 close 清理、stop-during-connect settle;slack webhook 二次 writeHead + messageChannelMap LRU。
14
+ - **Host/向导**:logs/stats 与 inbox 查询改 DB 侧 count/orderBy/limit 下推;save-yaml 写入前校验;zhipu/moonshot baseUrl 必填预填;.env 写入转义 + 幂等合并;setup 数据库密码不再明文落 config;60s fetch 超时与 JSON 守卫。
15
+
16
+ - 9c997b2: 通用 endpoint 管理命令套件:`@zhin.js/adapter` 新增 `createEndpointCommands(spec, defineCommand)`——`<adapter> endpoint list / add <name> key=value... / remove <name>` 三件套,含 kv 解析、`.env` 凭据派生(`<ADAPTER>_<NAME>_<FIELD>`)、yaml 写回保留注释、master 权限门禁(通用 `isEndpointOperator`)、自定义 bindFlow 钩子。QQ 迁移至套件(行为与扫码绑定流程不变);napcat / onebot11 / onebot12 / milky / slack / telegram 接入(字段对齐各自 schema,features 补 @zhin.js/command)。
17
+ - 09d4f25: Console 社交读取面(management 语义端口)多平台落地:napcat/onebot11/onebot12/milky(好友+群+群成员,OneBot 标准动作);discord/kook/satori(guild+频道+成员,分页聚合,id 保精度留字符串);slack(workspace 成员+public channels+conversations.members);line(群/room 成员分页+profile 回退);wechat-mp(followers openid);weixin-ilink(context_token 对端推导);lark(chats+members 全分页)。`EndpointFriend.user_id`/`EndpointGroup.group_id` 放宽为 `number | string`(雪花 id 不丢精度)。telegram/wecom/dingtalk/github/email/sandbox 注明平台无列表面暂不接。
18
+ - Updated dependencies [cdf64e7]
19
+ - Updated dependencies [2d0a159]
20
+ - Updated dependencies [5691aba]
21
+ - Updated dependencies [078e3f7]
22
+ - Updated dependencies [50497a5]
23
+ - Updated dependencies [9c997b2]
24
+ - Updated dependencies [09d4f25]
25
+ - Updated dependencies [43485a9]
26
+ - Updated dependencies [f0ec5ab]
27
+ - Updated dependencies [3e925d0]
28
+ - Updated dependencies [fa66c4c]
29
+ - Updated dependencies [fa66c4c]
30
+ - Updated dependencies [6cb6152]
31
+ - @zhin.js/command@1.0.3
32
+ - @zhin.js/agent@1.0.6
33
+ - @zhin.js/plugin-runtime@1.1.1
34
+ - @zhin.js/host-http@1.0.3
35
+ - zhin.js@5.0.1
36
+ - @zhin.js/adapter@1.1.1
37
+ - @zhin.js/core@1.4.1
38
+
3
39
  ## 5.0.0
4
40
 
5
41
  ### Patch Changes
package/adapters/slack.ts CHANGED
@@ -9,14 +9,26 @@ import {
9
9
  resolveSlackConfig,
10
10
  type SlackAdapterConfig,
11
11
  } from '../src/protocol.js';
12
+ import { slackRuntimeStateToken } from '../src/slack-runtime-state.js';
12
13
 
13
14
  export { SlackEndpoint } from '../src/endpoint.js';
14
15
  export type { SlackEndpointOptions, SlackSocketLike, SlackWebClientLike } from '../src/endpoint.js';
15
16
 
16
17
  export default defineAdapter<SlackAdapterConfig>({
17
18
  capabilities: ['inbound', 'outbound'],
19
+ // 媒体由端点物化(url 拉取 / 本地读取)经 files.uploadV2 上传;
20
+ // Block Kit 原生按钮承载交互段。
21
+ segments: {
22
+ outboundMedia: ['url', 'upload'],
23
+ interactive: 'native',
24
+ },
18
25
  create(context) {
19
26
  const config = resolveSlackConfig(context.config);
27
+ // 注册到插件运行时状态(slack endpoint list 的"运行中"数据源)
28
+ context.use(slackRuntimeStateToken).endpoints.set(config.name, {
29
+ name: config.name,
30
+ mode: config.mode,
31
+ });
20
32
  if (config.mode === 'http') {
21
33
  return new SlackEndpoint({
22
34
  id: context.id,
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { slackEndpointCommands } from '../../../src/slack-endpoint-commands.js';
2
+
3
+ export default slackEndpointCommands.remove;
package/lib/endpoint.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EndpointInstance } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -57,6 +57,17 @@ export interface SlackWebClientLike extends SlackChatClient {
57
57
  }): Promise<{
58
58
  channel?: unknown;
59
59
  }>;
60
+ list(opts?: {
61
+ types?: string;
62
+ limit?: number;
63
+ cursor?: string;
64
+ exclude_archived?: boolean;
65
+ }): Promise<{
66
+ channels?: unknown[];
67
+ response_metadata?: {
68
+ next_cursor?: string;
69
+ };
70
+ }>;
60
71
  };
61
72
  users: {
62
73
  info(opts: {
@@ -64,6 +75,15 @@ export interface SlackWebClientLike extends SlackChatClient {
64
75
  }): Promise<{
65
76
  user?: unknown;
66
77
  }>;
78
+ list(opts?: {
79
+ limit?: number;
80
+ cursor?: string;
81
+ }): Promise<{
82
+ members?: unknown[];
83
+ response_metadata?: {
84
+ next_cursor?: string;
85
+ };
86
+ }>;
67
87
  };
68
88
  reactions: {
69
89
  add(opts: {
@@ -107,6 +127,7 @@ export interface SlackEndpointOptions {
107
127
  }
108
128
  export declare class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
109
129
  #private;
130
+ readonly management: EndpointManagement;
110
131
  constructor(options: SlackEndpointOptions);
111
132
  /** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
112
133
  get name(): string;
package/lib/endpoint.js CHANGED
@@ -24,6 +24,7 @@ export class SlackEndpoint {
24
24
  #open = false;
25
25
  #started = false;
26
26
  #unregisterAgent;
27
+ management = createSlackEndpointManagement(this);
27
28
  constructor(options) {
28
29
  this.#options = options;
29
30
  }
@@ -210,8 +211,17 @@ export class SlackEndpoint {
210
211
  }
211
212
  }
212
213
  trackMessageChannel(ts, channel) {
213
- if (ts && channel)
214
- this.#messageChannelMap.set(ts, channel);
214
+ if (!ts || !channel)
215
+ return;
216
+ // LRU:超 1024 条淘汰最久未更新的记录,避免无界增长。
217
+ if (this.#messageChannelMap.has(ts))
218
+ this.#messageChannelMap.delete(ts);
219
+ this.#messageChannelMap.set(ts, channel);
220
+ if (this.#messageChannelMap.size > 1024) {
221
+ const oldest = this.#messageChannelMap.keys().next().value;
222
+ if (oldest != null)
223
+ this.#messageChannelMap.delete(oldest);
224
+ }
215
225
  }
216
226
  resolveMessageRef(messageId, channelHint) {
217
227
  const parsed = parseSlackMessageRef(messageId);
@@ -306,8 +316,12 @@ export class SlackEndpoint {
306
316
  }
307
317
  async #startSocket() {
308
318
  const { config } = this.#options;
309
- if (!config.appToken)
310
- throw new Error('Socket Mode requires appToken');
319
+ if (!config.appToken) {
320
+ throw new Error('Slack Socket Mode 需要 appToken(xapp- 前缀的 App-Level Token);注意 bot token 填 token 字段(xoxb- 前缀),两者不可混用');
321
+ }
322
+ if (!config.appToken.startsWith('xapp-')) {
323
+ throw new Error(`Slack appToken 格式不正确:Socket Mode 需要 xapp- 前缀的 App-Level Token(当前看起来是 ${config.appToken.slice(0, 5)}…,xoxb- 是 bot token,请填到 token 字段)`);
324
+ }
311
325
  this.#socket = this.#options.createSocket?.({
312
326
  appToken: config.appToken,
313
327
  clientPingTimeout: config.clientPingTimeout,
@@ -337,3 +351,76 @@ function parseSendTarget(target) {
337
351
  }
338
352
  return { channel: target };
339
353
  }
354
+ /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
355
+ const SLACK_LIST_PAGE_SIZE = 1000;
356
+ function createSlackEndpointManagement(endpoint) {
357
+ const requireClient = () => {
358
+ const client = endpoint.client;
359
+ if (!client)
360
+ throw new Error('Slack client not connected');
361
+ return client;
362
+ };
363
+ return Object.freeze({
364
+ // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
365
+ // Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
366
+ async listGroups() {
367
+ const client = requireClient();
368
+ const groups = [];
369
+ let cursor;
370
+ do {
371
+ const page = await client.conversations.list({
372
+ types: 'public_channel',
373
+ exclude_archived: true,
374
+ limit: SLACK_LIST_PAGE_SIZE,
375
+ ...(cursor ? { cursor } : {}),
376
+ });
377
+ for (const value of page.channels ?? []) {
378
+ const channel = asRecord(value);
379
+ const id = typeof channel.id === 'string' ? channel.id : '';
380
+ if (!id)
381
+ continue;
382
+ groups.push({ group_id: id, name: String(channel.name ?? id) });
383
+ }
384
+ cursor = page.response_metadata?.next_cursor || undefined;
385
+ } while (cursor);
386
+ return groups;
387
+ },
388
+ // Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
389
+ async listFriends() {
390
+ const client = requireClient();
391
+ const friends = [];
392
+ let cursor;
393
+ do {
394
+ const page = await client.users.list({
395
+ limit: SLACK_LIST_PAGE_SIZE,
396
+ ...(cursor ? { cursor } : {}),
397
+ });
398
+ for (const value of page.members ?? []) {
399
+ const user = asRecord(value);
400
+ const id = typeof user.id === 'string' ? user.id : '';
401
+ if (!id || user.deleted === true)
402
+ continue;
403
+ const profile = asRecord(user.profile);
404
+ friends.push({
405
+ user_id: id,
406
+ nickname: String(profile.real_name ?? user.real_name ?? user.name ?? id),
407
+ remark: '',
408
+ });
409
+ }
410
+ cursor = page.response_metadata?.next_cursor || undefined;
411
+ } while (cursor);
412
+ return friends;
413
+ },
414
+ // conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
415
+ async listGroupMembers(groupId) {
416
+ const client = requireClient();
417
+ const result = await client.conversations.members({ channel: groupId });
418
+ return result.members ?? [];
419
+ },
420
+ });
421
+ }
422
+ function asRecord(value) {
423
+ return value !== null && typeof value === 'object'
424
+ ? value
425
+ : {};
426
+ }
@@ -0,0 +1 @@
1
+ export declare const slackEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `slack endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ */
5
+ import { createEndpointCommands } from '@zhin.js/adapter';
6
+ import { defineCommand } from '@zhin.js/command';
7
+ import { slackRuntimeStateToken } from './slack-runtime-state.js';
8
+ export const slackEndpointCommands = createEndpointCommands({
9
+ adapterKey: 'slack',
10
+ adapterDisplayName: 'Slack',
11
+ fields: [
12
+ { key: 'token', required: true, env: true, description: 'Bot User OAuth Token(xoxb-...)' },
13
+ { key: 'signingSecret', env: true, description: '签名密钥(HTTP Events,socketMode: false 必填)' },
14
+ { key: 'appToken', env: true, description: 'App-Level Token(xapp-...,Socket Mode)' },
15
+ ],
16
+ running: (use) => use(slackRuntimeStateToken).endpoints.values(),
17
+ describeEntry: (entry) => `token: ${String(entry.token)}`,
18
+ }, defineCommand);
@@ -0,0 +1 @@
1
+ export declare const slackRuntimeStateToken: import("@zhin.js/plugin-runtime").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -0,0 +1,6 @@
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');
package/lib/webhook.js CHANGED
@@ -24,9 +24,10 @@ export async function handleSlackWebhookRequest(request, response, handler) {
24
24
  const params = new URLSearchParams(rawBody);
25
25
  const payloadStr = params.get('payload');
26
26
  if (payloadStr) {
27
+ const payload = JSON.parse(payloadStr);
27
28
  response.writeHead(200);
28
29
  response.end('');
29
- handler.admitInteraction(JSON.parse(payloadStr));
30
+ handler.admitInteraction(payload);
30
31
  return;
31
32
  }
32
33
  const body = Object.fromEntries(params);
@@ -63,7 +64,9 @@ export async function handleSlackWebhookRequest(request, response, handler) {
63
64
  }
64
65
  catch (error) {
65
66
  logger.error('Slack webhook error:', error);
66
- response.writeHead(200);
67
- response.end('');
67
+ if (!response.headersSent) {
68
+ response.writeHead(200);
69
+ response.end('');
70
+ }
68
71
  }
69
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-slack",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Zhin.js Slack adapter for Plugin Runtime (Socket Mode / HTTP Events)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,20 +34,21 @@
34
34
  "dependencies": {
35
35
  "@slack/socket-mode": "^2.0.3",
36
36
  "@slack/web-api": "^7.17.0",
37
- "@zhin.js/adapter": "1.1.0",
38
- "@zhin.js/core": "1.4.0",
39
- "@zhin.js/host-http": "1.0.2",
37
+ "@zhin.js/adapter": "1.1.1",
38
+ "@zhin.js/command": "1.0.3",
39
+ "@zhin.js/core": "1.4.1",
40
+ "@zhin.js/host-http": "1.0.3",
40
41
  "@zhin.js/logger": "1.0.75",
41
- "@zhin.js/plugin-runtime": "1.1.0"
42
+ "@zhin.js/plugin-runtime": "1.1.1"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "zod": "^4.0.0",
45
- "@zhin.js/adapter": "1.1.0",
46
- "@zhin.js/agent": "1.0.5",
47
- "@zhin.js/core": "1.4.0",
48
- "@zhin.js/host-http": "1.0.2",
49
- "@zhin.js/plugin-runtime": "1.1.0",
50
- "zhin.js": "5.0.0"
46
+ "@zhin.js/adapter": "1.1.1",
47
+ "@zhin.js/agent": "1.0.6",
48
+ "@zhin.js/core": "1.4.1",
49
+ "@zhin.js/host-http": "1.0.3",
50
+ "@zhin.js/plugin-runtime": "1.1.1",
51
+ "zhin.js": "5.0.1"
51
52
  },
52
53
  "peerDependenciesMeta": {
53
54
  "zhin.js": {
@@ -65,11 +66,12 @@
65
66
  "typescript": "^6.0.3",
66
67
  "vitest": "^4.1.10",
67
68
  "zod": "^4.4.3",
68
- "@zhin.js/agent": "1.0.5",
69
- "zhin.js": "5.0.0"
69
+ "@zhin.js/agent": "1.0.6",
70
+ "zhin.js": "5.0.1"
70
71
  },
71
72
  "files": [
72
73
  "adapters",
74
+ "commands",
73
75
  "plugin.ts",
74
76
  "schema.json",
75
77
  "src",
@@ -95,6 +97,10 @@
95
97
  {
96
98
  "package": "@zhin.js/adapter",
97
99
  "api": "^1.0.0"
100
+ },
101
+ {
102
+ "package": "@zhin.js/command",
103
+ "api": "^1.0.0"
98
104
  }
99
105
  ],
100
106
  "plugins": []
package/plugin.ts CHANGED
@@ -1,12 +1,16 @@
1
+ import { createEndpointRuntimeState } from '@zhin.js/adapter';
1
2
  import { definePlugin } from '@zhin.js/plugin-runtime';
2
3
  import { registerSlackPlatformPermitChecker } from './src/platform-permit.js';
4
+ import { slackRuntimeStateToken } from './src/slack-runtime-state.js';
3
5
 
4
6
  export default definePlugin({
5
7
  name: 'slack',
6
8
  metadata: {
7
9
  displayName: 'Slack Adapter',
8
10
  },
9
- setup() {
11
+ setup(context) {
12
+ // 运行中 endpoint 注册表(slack endpoint list 的"运行中"数据源)
13
+ context.resources.provide(slackRuntimeStateToken, createEndpointRuntimeState());
10
14
  // 平台权限门禁:workspace_owner / channel_manager 等(agent 工具 platformPermit)
11
15
  return registerSlackPlatformPermitChecker();
12
16
  },
package/src/endpoint.ts CHANGED
@@ -3,7 +3,12 @@
3
3
  */
4
4
  import { SocketModeClient } from '@slack/socket-mode';
5
5
  import { WebClient } from '@slack/web-api';
6
- import type { EndpointInstance } from '@zhin.js/adapter';
6
+ import type {
7
+ EndpointFriend,
8
+ EndpointGroup,
9
+ EndpointInstance,
10
+ EndpointManagement,
11
+ } from '@zhin.js/adapter';
7
12
  import type { MessageGateway } from '@zhin.js/core/runtime';
8
13
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
9
14
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -57,9 +62,19 @@ export interface SlackWebClientLike extends SlackChatClient {
57
62
  rename(opts: { channel: string; name: string }): Promise<unknown>;
58
63
  members(opts: { channel: string }): Promise<{ members?: string[] }>;
59
64
  info(opts: { channel: string }): Promise<{ channel?: unknown }>;
65
+ list(opts?: {
66
+ types?: string;
67
+ limit?: number;
68
+ cursor?: string;
69
+ exclude_archived?: boolean;
70
+ }): Promise<{ channels?: unknown[]; response_metadata?: { next_cursor?: string } }>;
60
71
  };
61
72
  users: {
62
73
  info(opts: { user: string }): Promise<{ user?: unknown }>;
74
+ list(opts?: {
75
+ limit?: number;
76
+ cursor?: string;
77
+ }): Promise<{ members?: unknown[]; response_metadata?: { next_cursor?: string } }>;
63
78
  };
64
79
  reactions: {
65
80
  add(opts: { channel: string; timestamp: string; name: string }): Promise<unknown>;
@@ -97,6 +112,7 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
97
112
  #open = false;
98
113
  #started = false;
99
114
  #unregisterAgent?: () => void;
115
+ readonly management: EndpointManagement = createSlackEndpointManagement(this);
100
116
 
101
117
  constructor(options: SlackEndpointOptions) {
102
118
  this.#options = options;
@@ -293,7 +309,14 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
293
309
  }
294
310
 
295
311
  trackMessageChannel(ts: string, channel: string): void {
296
- if (ts && channel) this.#messageChannelMap.set(ts, channel);
312
+ if (!ts || !channel) return;
313
+ // LRU:超 1024 条淘汰最久未更新的记录,避免无界增长。
314
+ if (this.#messageChannelMap.has(ts)) this.#messageChannelMap.delete(ts);
315
+ this.#messageChannelMap.set(ts, channel);
316
+ if (this.#messageChannelMap.size > 1024) {
317
+ const oldest = this.#messageChannelMap.keys().next().value;
318
+ if (oldest != null) this.#messageChannelMap.delete(oldest);
319
+ }
297
320
  }
298
321
 
299
322
  resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
@@ -400,7 +423,12 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
400
423
 
401
424
  async #startSocket(): Promise<void> {
402
425
  const { config } = this.#options;
403
- if (!config.appToken) throw new Error('Socket Mode requires appToken');
426
+ if (!config.appToken) {
427
+ throw new Error('Slack Socket Mode 需要 appToken(xapp- 前缀的 App-Level Token);注意 bot token 填 token 字段(xoxb- 前缀),两者不可混用');
428
+ }
429
+ if (!config.appToken.startsWith('xapp-')) {
430
+ throw new Error(`Slack appToken 格式不正确:Socket Mode 需要 xapp- 前缀的 App-Level Token(当前看起来是 ${config.appToken.slice(0, 5)}…,xoxb- 是 bot token,请填到 token 字段)`);
431
+ }
404
432
  this.#socket = this.#options.createSocket?.({
405
433
  appToken: config.appToken,
406
434
  clientPingTimeout: config.clientPingTimeout,
@@ -433,3 +461,76 @@ function parseSendTarget(target: string): { channel: string; threadTs?: string }
433
461
  }
434
462
  return { channel: target };
435
463
  }
464
+
465
+ /** Slack Web API 分页上限(每页 1000,cursor 翻页直到 next_cursor 为空)。 */
466
+ const SLACK_LIST_PAGE_SIZE = 1000;
467
+
468
+ function createSlackEndpointManagement(endpoint: SlackEndpoint): EndpointManagement {
469
+ const requireClient = (): SlackWebClientLike => {
470
+ const client = endpoint.client;
471
+ if (!client) throw new Error('Slack client not connected');
472
+ return client;
473
+ };
474
+ return Object.freeze<EndpointManagement>({
475
+ // Slack 无"群"概念:public channel 归一为 group(channel 语义由 listChannels 之外省略,
476
+ // Slack channel 本身就是会话载体,避免同一批数据在两个列表里重复)。
477
+ async listGroups(): Promise<readonly EndpointGroup[]> {
478
+ const client = requireClient();
479
+ const groups: EndpointGroup[] = [];
480
+ let cursor: string | undefined;
481
+ do {
482
+ const page = await client.conversations.list({
483
+ types: 'public_channel',
484
+ exclude_archived: true,
485
+ limit: SLACK_LIST_PAGE_SIZE,
486
+ ...(cursor ? { cursor } : {}),
487
+ });
488
+ for (const value of page.channels ?? []) {
489
+ const channel = asRecord(value);
490
+ const id = typeof channel.id === 'string' ? channel.id : '';
491
+ if (!id) continue;
492
+ groups.push({ group_id: id, name: String(channel.name ?? id) });
493
+ }
494
+ cursor = page.response_metadata?.next_cursor || undefined;
495
+ } while (cursor);
496
+ return groups;
497
+ },
498
+ // Slack 无"好友"概念:workspace 成员归一为 friend;nickname 取 real_name 回退 name。
499
+ async listFriends(): Promise<readonly EndpointFriend[]> {
500
+ const client = requireClient();
501
+ const friends: EndpointFriend[] = [];
502
+ let cursor: string | undefined;
503
+ do {
504
+ const page = await client.users.list({
505
+ limit: SLACK_LIST_PAGE_SIZE,
506
+ ...(cursor ? { cursor } : {}),
507
+ });
508
+ for (const value of page.members ?? []) {
509
+ const user = asRecord(value);
510
+ const id = typeof user.id === 'string' ? user.id : '';
511
+ if (!id || user.deleted === true) continue;
512
+ const profile = asRecord(user.profile);
513
+ friends.push({
514
+ user_id: id,
515
+ nickname: String(profile.real_name ?? user.real_name ?? user.name ?? id),
516
+ remark: '',
517
+ });
518
+ }
519
+ cursor = page.response_metadata?.next_cursor || undefined;
520
+ } while (cursor);
521
+ return friends;
522
+ },
523
+ // conversations.members 只回 user id 列表(平台形状),逐 user 拉 profile 成本由调用方决定。
524
+ async listGroupMembers(groupId: string): Promise<readonly string[]> {
525
+ const client = requireClient();
526
+ const result = await client.conversations.members({ channel: groupId });
527
+ return result.members ?? [];
528
+ },
529
+ });
530
+ }
531
+
532
+ function asRecord(value: unknown): Record<string, unknown> {
533
+ return value !== null && typeof value === 'object'
534
+ ? value as Record<string, unknown>
535
+ : {};
536
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `slack endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ */
5
+ import { createEndpointCommands } from '@zhin.js/adapter';
6
+ import { defineCommand } from '@zhin.js/command';
7
+ import { slackRuntimeStateToken } from './slack-runtime-state.js';
8
+
9
+ export const slackEndpointCommands = createEndpointCommands({
10
+ adapterKey: 'slack',
11
+ adapterDisplayName: 'Slack',
12
+ fields: [
13
+ { key: 'token', required: true, env: true, description: 'Bot User OAuth Token(xoxb-...)' },
14
+ { key: 'signingSecret', env: true, description: '签名密钥(HTTP Events,socketMode: false 必填)' },
15
+ { key: 'appToken', env: true, description: 'App-Level Token(xapp-...,Socket Mode)' },
16
+ ],
17
+ running: (use) => use(slackRuntimeStateToken).endpoints.values(),
18
+ describeEntry: (entry) => `token: ${String(entry.token)}`,
19
+ }, defineCommand);
@@ -0,0 +1,7 @@
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');
package/src/webhook.ts CHANGED
@@ -56,9 +56,10 @@ export async function handleSlackWebhookRequest(
56
56
  const params = new URLSearchParams(rawBody);
57
57
  const payloadStr = params.get('payload');
58
58
  if (payloadStr) {
59
+ const payload = JSON.parse(payloadStr) as SlackInteractionPayload;
59
60
  response.writeHead(200);
60
61
  response.end('');
61
- handler.admitInteraction(JSON.parse(payloadStr) as SlackInteractionPayload);
62
+ handler.admitInteraction(payload);
62
63
  return;
63
64
  }
64
65
  const body = Object.fromEntries(params) as unknown as SlackSlashCommand;
@@ -97,7 +98,9 @@ export async function handleSlackWebhookRequest(
97
98
  response.end('');
98
99
  } catch (error) {
99
100
  logger.error('Slack webhook error:', error);
100
- response.writeHead(200);
101
- response.end('');
101
+ if (!response.headersSent) {
102
+ response.writeHead(200);
103
+ response.end('');
104
+ }
102
105
  }
103
106
  }