@zhin.js/adapter-slack 4.1.1 → 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 +66 -0
- package/README.md +22 -6
- package/adapters/slack.ts +12 -0
- package/commands/endpoint/add/[name:string].ts +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[name:string].ts +3 -0
- package/lib/endpoint.d.ts +24 -1
- package/lib/endpoint.js +95 -4
- package/lib/slack-endpoint-commands.d.ts +1 -0
- package/lib/slack-endpoint-commands.js +18 -0
- package/lib/slack-runtime-state.d.ts +1 -0
- package/lib/slack-runtime-state.js +6 -0
- package/lib/webhook.js +6 -3
- package/package.json +19 -13
- package/plugin.ts +5 -1
- package/schema.json +38 -17
- package/src/endpoint.ts +109 -3
- package/src/slack-endpoint-commands.ts +19 -0
- package/src/slack-runtime-state.ts +7 -0
- package/src/webhook.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
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
|
+
|
|
39
|
+
## 5.0.0
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
|
|
44
|
+
|
|
45
|
+
BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
|
|
46
|
+
|
|
47
|
+
- 1ddcd70: Console/契约扫尾批:
|
|
48
|
+
|
|
49
|
+
- adapter:多 endpoint record name 改为 entry.name(Console 展示/resolve/inbox 按名唯一命中);`expandEndpointConfigs` 增加缺名/非法字符/重名校验与告警;slack endpoint 补 `name` getter;inbox-installer / agent-host 解析 `slot~entry` 展开 id(activity-feedback 随之恢复)。
|
|
50
|
+
- host-http:`schema:get`/`config:get` 兼容 `data.plugin`;extended RPC 参数顶层与 `data` 合并(cron 写操作修复);请求审批认 `requestId`、已读认单值 `id`;`endpoint:requests`/`inboxRequests`/`inboxNotices` 行补 camelCase/扁平别名;cron 列表补 `expression/running/plugin/nextExecution/createdAt/context`。
|
|
51
|
+
- cli:装配 `setOrchestrationRuntime`/`setSessionTreeRuntime`(agent-sessions/orchestration 页恢复);`/api/stats` 补 commands/components 计数;console REST databaseHost.started 改动态 getter;`wrapModel` 支持 orderBy/limit 链式查询;接线 SystemLog 模型 + 日志 transport(logs 页有真实数据,带 7 天/1 万条清理)。
|
|
52
|
+
- plugin-runtime:`DatabaseHostModel.select` 升级为链式 `DatabaseHostSelection`;新增 `system-log`(SystemLog 表定义/写入助手)。
|
|
53
|
+
- agent:导出 `asPrivate`(Runtime Host 装配 session tree runtime 用)。
|
|
54
|
+
|
|
55
|
+
- 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
|
|
56
|
+
- f32c424: 适配器配置统一为 endpoints 数组格式:`plugins.<adapter>` 为该 adapter 所有 endpoint 的通用配置,`plugins.<adapter>.endpoints[i]` 为单个 endpoint 的特殊配置(逐项覆盖,`name` 必填)。slack / github schema 新增 `endpoints` 属性;icqq / qq / slack 顶层必填改为 `anyOf`(单 endpoint 字段或 `endpoints` 数组二选一,兼容 Ajv strictRequired)。
|
|
57
|
+
- Updated dependencies [7db69c1]
|
|
58
|
+
- Updated dependencies [e5c84ed]
|
|
59
|
+
- Updated dependencies [3ea84a0]
|
|
60
|
+
- Updated dependencies [1ddcd70]
|
|
61
|
+
- Updated dependencies [ac9da66]
|
|
62
|
+
- @zhin.js/core@1.4.0
|
|
63
|
+
- @zhin.js/adapter@1.1.0
|
|
64
|
+
- @zhin.js/plugin-runtime@1.1.0
|
|
65
|
+
- @zhin.js/agent@1.0.5
|
|
66
|
+
- @zhin.js/host-http@1.0.2
|
|
67
|
+
- zhin.js@5.0.0
|
|
68
|
+
|
|
3
69
|
## 4.1.1
|
|
4
70
|
|
|
5
71
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -44,10 +44,25 @@ pnpm add @zhin.js/adapter-slack
|
|
|
44
44
|
# zhin.config.yml(Plugin Runtime)
|
|
45
45
|
plugins:
|
|
46
46
|
slack:
|
|
47
|
-
name: my-slack-bot
|
|
48
|
-
token: ${SLACK_BOT_TOKEN}
|
|
49
|
-
appToken: ${SLACK_APP_TOKEN}
|
|
50
47
|
socketMode: true # 默认 true,可省略
|
|
48
|
+
endpoints:
|
|
49
|
+
- name: my-slack-bot
|
|
50
|
+
token: ${SLACK_BOT_TOKEN}
|
|
51
|
+
appToken: ${SLACK_APP_TOKEN}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
多 workspace:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`name` 必填):
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
plugins:
|
|
58
|
+
slack:
|
|
59
|
+
endpoints:
|
|
60
|
+
- name: team-a
|
|
61
|
+
token: ${SLACK_BOT_TOKEN_A}
|
|
62
|
+
appToken: ${SLACK_APP_TOKEN_A}
|
|
63
|
+
- name: team-b
|
|
64
|
+
token: ${SLACK_BOT_TOKEN_B}
|
|
65
|
+
appToken: ${SLACK_APP_TOKEN_B}
|
|
51
66
|
```
|
|
52
67
|
|
|
53
68
|
## HTTP Events 配置
|
|
@@ -55,11 +70,12 @@ plugins:
|
|
|
55
70
|
```yaml
|
|
56
71
|
plugins:
|
|
57
72
|
slack:
|
|
58
|
-
name: my-slack-bot
|
|
59
|
-
token: ${SLACK_BOT_TOKEN}
|
|
60
|
-
signingSecret: ${SLACK_SIGNING_SECRET}
|
|
61
73
|
socketMode: false
|
|
62
74
|
webhookPath: /slack/events # 可选,默认 /slack/events
|
|
75
|
+
endpoints:
|
|
76
|
+
- name: my-slack-bot
|
|
77
|
+
token: ${SLACK_BOT_TOKEN}
|
|
78
|
+
signingSecret: ${SLACK_SIGNING_SECRET}
|
|
63
79
|
```
|
|
64
80
|
|
|
65
81
|
根插件 `zhin.plugins`(或项目图)需引用 `@zhin.js/adapter-slack`(`instanceKey: slack`)。
|
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,
|
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,7 +127,10 @@ 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);
|
|
132
|
+
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
133
|
+
get name(): string;
|
|
111
134
|
get client(): SlackWebClientLike | undefined;
|
|
112
135
|
get platformUserId(): string | undefined;
|
|
113
136
|
get config(): ResolvedSlackConfig;
|
package/lib/endpoint.js
CHANGED
|
@@ -24,9 +24,14 @@ 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
|
}
|
|
31
|
+
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
32
|
+
get name() {
|
|
33
|
+
return this.#options.config.name;
|
|
34
|
+
}
|
|
30
35
|
get client() {
|
|
31
36
|
return this.#client;
|
|
32
37
|
}
|
|
@@ -206,8 +211,17 @@ export class SlackEndpoint {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
trackMessageChannel(ts, channel) {
|
|
209
|
-
if (ts
|
|
210
|
-
|
|
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
|
+
}
|
|
211
225
|
}
|
|
212
226
|
resolveMessageRef(messageId, channelHint) {
|
|
213
227
|
const parsed = parseSlackMessageRef(messageId);
|
|
@@ -302,8 +316,12 @@ export class SlackEndpoint {
|
|
|
302
316
|
}
|
|
303
317
|
async #startSocket() {
|
|
304
318
|
const { config } = this.#options;
|
|
305
|
-
if (!config.appToken)
|
|
306
|
-
throw new Error('Socket Mode
|
|
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
|
+
}
|
|
307
325
|
this.#socket = this.#options.createSocket?.({
|
|
308
326
|
appToken: config.appToken,
|
|
309
327
|
clientPingTimeout: config.clientPingTimeout,
|
|
@@ -333,3 +351,76 @@ function parseSendTarget(target) {
|
|
|
333
351
|
}
|
|
334
352
|
return { channel: target };
|
|
335
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(
|
|
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.
|
|
67
|
-
|
|
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": "
|
|
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.
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
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.
|
|
42
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"zod": "^4.0.0",
|
|
45
|
-
"@zhin.js/adapter": "1.
|
|
46
|
-
"@zhin.js/agent": "1.0.
|
|
47
|
-
"@zhin.js/core": "1.
|
|
48
|
-
"@zhin.js/host-http": "1.0.
|
|
49
|
-
"@zhin.js/plugin-runtime": "1.
|
|
50
|
-
"zhin.js": "
|
|
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.
|
|
69
|
-
"zhin.js": "
|
|
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/schema.json
CHANGED
|
@@ -3,22 +3,6 @@
|
|
|
3
3
|
"type": "object",
|
|
4
4
|
"additionalProperties": false,
|
|
5
5
|
"properties": {
|
|
6
|
-
"name": {
|
|
7
|
-
"type": "string",
|
|
8
|
-
"default": "slack-bot"
|
|
9
|
-
},
|
|
10
|
-
"token": {
|
|
11
|
-
"type": "string",
|
|
12
|
-
"description": "Bot User OAuth Token (xoxb-...)"
|
|
13
|
-
},
|
|
14
|
-
"signingSecret": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"description": "Required for HTTP Events API (socketMode: false)"
|
|
17
|
-
},
|
|
18
|
-
"appToken": {
|
|
19
|
-
"type": "string",
|
|
20
|
-
"description": "App-Level Token (xapp-...) for Socket Mode"
|
|
21
|
-
},
|
|
22
6
|
"socketMode": {
|
|
23
7
|
"type": "boolean",
|
|
24
8
|
"default": true,
|
|
@@ -32,7 +16,44 @@
|
|
|
32
16
|
"type": "number",
|
|
33
17
|
"default": 15000,
|
|
34
18
|
"description": "Socket Mode client ping timeout (ms)"
|
|
19
|
+
},
|
|
20
|
+
"endpoints": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": true,
|
|
26
|
+
"properties": {
|
|
27
|
+
"name": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Slack bot name"
|
|
30
|
+
},
|
|
31
|
+
"token": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Bot User OAuth Token (xoxb-...)"
|
|
34
|
+
},
|
|
35
|
+
"signingSecret": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Required for HTTP Events API (socketMode: false)"
|
|
38
|
+
},
|
|
39
|
+
"appToken": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "App-Level Token (xapp-...) for Socket Mode"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
"name",
|
|
46
|
+
"token"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"commandPrefix": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"default": "",
|
|
53
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
35
54
|
}
|
|
36
55
|
},
|
|
37
|
-
"required": [
|
|
56
|
+
"required": [
|
|
57
|
+
"endpoints"
|
|
58
|
+
]
|
|
38
59
|
}
|
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 {
|
|
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,11 +112,17 @@ 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;
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
/** Console 展示 / AdapterIndex live name(多 endpoint 时与 entry name 一致)。 */
|
|
122
|
+
get name(): string {
|
|
123
|
+
return this.#options.config.name;
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
get client(): SlackWebClientLike | undefined {
|
|
106
127
|
return this.#client;
|
|
107
128
|
}
|
|
@@ -288,7 +309,14 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
|
|
|
288
309
|
}
|
|
289
310
|
|
|
290
311
|
trackMessageChannel(ts: string, channel: string): void {
|
|
291
|
-
if (ts
|
|
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
|
+
}
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
|
|
@@ -395,7 +423,12 @@ export class SlackEndpoint implements EndpointInstance, SlackWebhookHandler {
|
|
|
395
423
|
|
|
396
424
|
async #startSocket(): Promise<void> {
|
|
397
425
|
const { config } = this.#options;
|
|
398
|
-
if (!config.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
|
+
}
|
|
399
432
|
this.#socket = this.#options.createSocket?.({
|
|
400
433
|
appToken: config.appToken,
|
|
401
434
|
clientPingTimeout: config.clientPingTimeout,
|
|
@@ -428,3 +461,76 @@ function parseSendTarget(target: string): { channel: string; threadTs?: string }
|
|
|
428
461
|
}
|
|
429
462
|
return { channel: target };
|
|
430
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(
|
|
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.
|
|
101
|
-
|
|
101
|
+
if (!response.headersSent) {
|
|
102
|
+
response.writeHead(200);
|
|
103
|
+
response.end('');
|
|
104
|
+
}
|
|
102
105
|
}
|
|
103
106
|
}
|