dsh-slack 0.1.0 → 0.2.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/README.en.md +174 -0
- package/README.md +70 -18
- package/cordis.patch.yml +3 -0
- package/lib/config.d.ts +10 -0
- package/lib/config.js +15 -2
- package/lib/inbox.d.ts +38 -0
- package/lib/inbox.js +99 -0
- package/lib/index.d.ts +8 -5
- package/lib/index.js +52 -14
- package/lib/slack-client.d.ts +3 -1
- package/lib/slack-client.js +9 -2
- package/lib/socket.d.ts +16 -0
- package/lib/socket.js +44 -0
- package/lib/tools.d.ts +4 -8
- package/lib/tools.js +99 -13
- package/lib/types.d.ts +6 -0
- package/package.json +7 -4
package/README.en.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# dsh-slack
|
|
2
|
+
|
|
3
|
+
DSH (DeepSeek Harness) community plugin: lets the agent communicate with Slack bidirectionally.
|
|
4
|
+
|
|
5
|
+
> **v0.2 scope (two-way)**: v0.1 only did one-way "agent → Slack" notifications; v0.2 adds Socket Mode,
|
|
6
|
+
> enabling "Slack message → agent": `slack_inbox` receives messages, `slack_reply` replies in threads.
|
|
7
|
+
> RTM and interactive components (buttons/modals/slash command replies) are out of scope for v0.2 — see
|
|
8
|
+
> [Known limitations and roadmap](#known-limitations-and-roadmap) below.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- `slack_notify`: send a Markdown text message to a channel (or thread), returning the message `ts`.
|
|
13
|
+
- `slack_channels`: list the channels currently visible to the bot (`conversations.list`).
|
|
14
|
+
- `slack_inbox`: read messages received via Socket Mode (in-memory queue, keeps up to 200).
|
|
15
|
+
- `slack_reply`: reply to an inbox message as a thread (`chat.postMessage` with `thread_ts`).
|
|
16
|
+
- Configuration via `cordis.patch.yml`; tokens support environment-variable fallback (`DSH_SLACK_TOKEN` / `DSH_SLACK_APP_TOKEN`).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
The plugin runs inside the host process, is installed into the profile via `dsh plugin`, and takes effect after a restart:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
dsh plugin --profile web add dsh-slack
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
After installing, restart your dsh Web service; the four tools `slack_notify` / `slack_channels` / `slack_inbox` / `slack_reply` become visible to the model.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Configuration lives in the profile's `cordis.patch.yml`; override this plugin's line by `id: slack` (overriding replaces that line's `config` wholesale — it does not merge). Available options:
|
|
31
|
+
|
|
32
|
+
| Key | Type | Required | Description |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `token` | string | yes* | Slack token: bot token (`xoxb-`) or user token (`xoxp-`). When empty, falls back to the `DSH_SLACK_TOKEN` env var. |
|
|
35
|
+
| `appToken` | string | no* | App-Level Token (starts with `xapp-`); used to receive messages after enabling Socket Mode. When empty, falls back to the `DSH_SLACK_APP_TOKEN` env var. |
|
|
36
|
+
| `defaultChannel` | string | no | Default channel (e.g. `#general`). The default target when the model doesn't specify a channel; written into the `channel` parameter description. |
|
|
37
|
+
|
|
38
|
+
`*` `token` may be left empty at the "config layer", in which case it falls back to the env var; when both are empty the plugin still loads, but calling the send/list/reply tools returns a Chinese error. An empty `appToken` only warns — no crash — and `slack_inbox` returns an empty queue (one-way mode).
|
|
39
|
+
|
|
40
|
+
**Method 1: environment variables (recommended, no hard-coded tokens)**
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# 在启动 dsh 的进程里设置
|
|
44
|
+
export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
45
|
+
export DSH_SLACK_APP_TOKEN=xapp-你的App级令牌
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Method 2: override in the profile's cordis.patch.yml**
|
|
49
|
+
|
|
50
|
+
In your profile directory (`$DSH_HOME/profiles/web/cordis.patch.yml`) append:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# 覆盖 dsh-slack 的 slack 行配置(整体替换)
|
|
54
|
+
- id: slack
|
|
55
|
+
config:
|
|
56
|
+
token: 'xoxb-你的机器人令牌'
|
|
57
|
+
appToken: 'xapp-你的App级令牌'
|
|
58
|
+
defaultChannel: '#general'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Priority: `config.token` > env var `DSH_SLACK_TOKEN`; `config.appToken` > env var
|
|
62
|
+
> `DSH_SLACK_APP_TOKEN`.
|
|
63
|
+
|
|
64
|
+
### Create a Slack App and get a token
|
|
65
|
+
|
|
66
|
+
1. Open <https://api.slack.com/apps>, click **Create New App** (choose `From scratch`, name the app, select the workspace).
|
|
67
|
+
2. On the **OAuth & Permissions** page, under **Scopes → Bot Token Scopes** check:
|
|
68
|
+
- `chat:write` (required to send messages)
|
|
69
|
+
- `channels:read` (required to list channels)
|
|
70
|
+
3. Back at the top, click **Install to Workspace** (authorize).
|
|
71
|
+
4. Grab the **Bot User OAuth Token** (starts with `xoxb-`).
|
|
72
|
+
5. Add the bot (App) to the channels it should post in: `/invite @your-bot` in the channel (required for private channels).
|
|
73
|
+
|
|
74
|
+
### Enable Socket Mode (two-way message receiving)
|
|
75
|
+
|
|
76
|
+
To receive Slack messages (`slack_inbox` / `slack_reply`), enable Socket Mode and generate an App-Level Token:
|
|
77
|
+
|
|
78
|
+
1. Open <https://api.slack.com/apps> and open your App.
|
|
79
|
+
2. Open the **Socket Mode** page → turn on the toggle (**Enable Socket Mode**).
|
|
80
|
+
3. Click **Generate Token and Scopes** to create an App-Level Token: name the token, check the `connections:write` scope.
|
|
81
|
+
Copy the `xapp-` App-Level Token (shown only once — save it immediately).
|
|
82
|
+
4. Open the **Event Subscriptions** page, enable events, and under **Subscribe to bot events → Add Bot User Event** add:
|
|
83
|
+
- `message.channels` (public channel messages)
|
|
84
|
+
- `message.im` (bot DMs)
|
|
85
|
+
5. Put the App-Level Token into `appToken` (or the `DSH_SLACK_APP_TOKEN` env var).
|
|
86
|
+
6. Restart the dsh Web service; the plugin connects automatically via Socket Mode and starts receiving messages.
|
|
87
|
+
|
|
88
|
+
> Without `appToken` the plugin **won't crash**: it only prints a warning and `slack_inbox` returns an empty queue (with a Chinese hint).
|
|
89
|
+
> Socket Mode network errors are auto-reconnected by the SDK; the plugin only logs a warning and never throws.
|
|
90
|
+
|
|
91
|
+
## Tool reference
|
|
92
|
+
|
|
93
|
+
### `slack_notify`
|
|
94
|
+
|
|
95
|
+
Send Markdown text to a channel/thread (underlying `chat.postMessage`).
|
|
96
|
+
|
|
97
|
+
| Param | Type | Required | Description |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| `channel` | string | yes | Channel name (e.g. `#general`) or channel ID. |
|
|
100
|
+
| `text` | string | yes | The Markdown text to send. |
|
|
101
|
+
| `thread_ts` | string | no | The `ts` of the thread to reply to. |
|
|
102
|
+
|
|
103
|
+
Returns: `{ "ts": "...", "channel": "#general" }` (`ts` for later `thread_ts` reference).
|
|
104
|
+
|
|
105
|
+
### `slack_channels`
|
|
106
|
+
|
|
107
|
+
List channels visible to the bot (underlying `conversations.list`).
|
|
108
|
+
|
|
109
|
+
No parameters. Returns: `{ "channels": [{ "id": "...", "name": "..." }, ...] }`.
|
|
110
|
+
|
|
111
|
+
### `slack_inbox`
|
|
112
|
+
|
|
113
|
+
Read messages received via Socket Mode (in-memory queue, keeps up to 200, newest first).
|
|
114
|
+
|
|
115
|
+
| Param | Type | Required | Description |
|
|
116
|
+
| --- | --- | --- | --- |
|
|
117
|
+
| `limit` | integer | no | Maximum number of messages to return (default 10, range 1-50). |
|
|
118
|
+
| `markRead` | boolean | no | When `true`, clears the inbox queue after returning (mark as read). |
|
|
119
|
+
|
|
120
|
+
Returns: `{ "messages": [{ "ts": "...", "channel": "...", "user": "...", "text": "..." }, ...] }`.
|
|
121
|
+
|
|
122
|
+
### `slack_reply`
|
|
123
|
+
|
|
124
|
+
Reply to an inbox message as a thread (underlying `chat.postMessage` with `thread_ts`).
|
|
125
|
+
|
|
126
|
+
| Param | Type | Required | Description |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `channel` | string | yes | Channel name (e.g. `#general`) or channel ID. |
|
|
129
|
+
| `text` | string | yes | Reply content (Markdown text). |
|
|
130
|
+
| `thread_ts` | string | yes | The `ts` of the message to reply to (from `slack_inbox`). |
|
|
131
|
+
|
|
132
|
+
Returns: `{ "ts": "...", "channel": "#general" }`.
|
|
133
|
+
|
|
134
|
+
## Error handling
|
|
135
|
+
|
|
136
|
+
All error messages are in Chinese, readable by both the model and the user:
|
|
137
|
+
|
|
138
|
+
- Unconfigured token: `token 未配置:缺少 Slack 机器人令牌(xoxb-)…请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。`
|
|
139
|
+
- Unconfigured App-Level Token: only a warning; `slack_inbox` returns an empty queue (with a Chinese hint) and other tools are unaffected.
|
|
140
|
+
- `invalid_auth`: hints to check/regenerate the token.
|
|
141
|
+
- `channel_not_found`: hints the channel name or ID is wrong.
|
|
142
|
+
- `not_in_channel`: hints to invite the bot App into the channel first.
|
|
143
|
+
- `token_revoked` / `account_inactive` / `missing_scope`: hints permission or token expiry, reinstall the App.
|
|
144
|
+
|
|
145
|
+
## Development and testing
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
pnpm install # 安装依赖并触发 prepare(tsc 构建)
|
|
149
|
+
pnpm build # tsc 编译 src → lib
|
|
150
|
+
pnpm test # 先 build,再用 node:test 跑 test/*.test.mjs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Tests need no real token: parameter compilation, config parsing (incl. env fallback), tool registration (4 tools + Chinese error on missing config),
|
|
154
|
+
injecting a fake client to assert `postMessage` arguments (incl. `thread_ts`), output schema pure-JSON validation,
|
|
155
|
+
inbox queue capacity/clear, Socket Mode event parsing (fake event objects), and no-crash on missing appToken.
|
|
156
|
+
|
|
157
|
+
## Known limitations and roadmap
|
|
158
|
+
|
|
159
|
+
- **v0.1 is one-way notification (agent→Slack)**; **v0.2 is two-way via Socket Mode** (`slack_inbox` / `slack_reply`).
|
|
160
|
+
- **No** RTM or interactive components (buttons/modals/slash command replies).
|
|
161
|
+
- `slack_inbox` is an in-process memory queue: cleared on restart, not persisted; keeps up to 200, dropping the oldest when full.
|
|
162
|
+
- `channel` is a required parameter; `defaultChannel` is currently only written into the `channel` parameter description as a hint — it does not replace the required `channel`.
|
|
163
|
+
- With a missing token the plugin still loads (lazy loading); the error is only thrown when the tool is called; a missing appToken only warns.
|
|
164
|
+
|
|
165
|
+
Roadmap: v0.3 plans to introduce interactive components (buttons/modals) and a persisted inbox.
|
|
166
|
+
|
|
167
|
+
## Dependencies
|
|
168
|
+
|
|
169
|
+
- Runtime: `@slack/web-api` (official WebClient), `@slack/socket-mode` (Socket Mode client)
|
|
170
|
+
- peer (provided by the host; not imported directly by the plugin at runtime): `@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
[English](README.en.md)
|
|
2
|
+
|
|
1
3
|
# dsh-slack
|
|
2
4
|
|
|
3
|
-
DSH(DeepSeek Harness)社区插件:让 agent
|
|
5
|
+
DSH(DeepSeek Harness)社区插件:让 agent 与 Slack 双向通信。
|
|
4
6
|
|
|
5
|
-
> **v0.1
|
|
6
|
-
>
|
|
7
|
+
> **v0.2 范围(双向)**:v0.1 只做「agent → Slack」单向通知;v0.2 新增 Socket Mode,
|
|
8
|
+
> 支持「Slack 消息 → agent」:`slack_inbox` 收取消息、`slack_reply` 线程回复。
|
|
9
|
+
> RTM、交互组件(按钮/弹窗/slash command 回复)不在 v0.2,见下方
|
|
7
10
|
> [已知限制与路线图](#已知限制与路线图)。
|
|
8
11
|
|
|
9
12
|
## 功能
|
|
10
13
|
|
|
11
|
-
- `slack_notify`:向指定频道(或线程)发送一条 Markdown 文本消息,返回消息 `ts
|
|
14
|
+
- `slack_notify`:向指定频道(或线程)发送一条 Markdown 文本消息,返回消息 `ts`。
|
|
12
15
|
- `slack_channels`:列出机器人当前可见的频道(`conversations.list`)。
|
|
13
|
-
-
|
|
16
|
+
- `slack_inbox`:读取通过 Socket Mode 收到的消息(内存队列,最多保留 200 条)。
|
|
17
|
+
- `slack_reply`:以线程回复形式回复某条收件箱消息(`chat.postMessage` 带 `thread_ts`)。
|
|
18
|
+
- 配置走 `cordis.patch.yml`,令牌支持环境变量回退(`DSH_SLACK_TOKEN` / `DSH_SLACK_APP_TOKEN`)。
|
|
14
19
|
|
|
15
20
|
## 安装
|
|
16
21
|
|
|
@@ -20,7 +25,8 @@ DSH(DeepSeek Harness)社区插件:让 agent 向 Slack 发通知/桥接消
|
|
|
20
25
|
dsh plugin --profile web add dsh-slack
|
|
21
26
|
```
|
|
22
27
|
|
|
23
|
-
安装后重启你的 dsh Web 服务,`slack_notify` / `slack_channels`
|
|
28
|
+
安装后重启你的 dsh Web 服务,`slack_notify` / `slack_channels` / `slack_inbox` / `slack_reply`
|
|
29
|
+
四个工具即对模型可见。
|
|
24
30
|
|
|
25
31
|
## 配置
|
|
26
32
|
|
|
@@ -30,16 +36,18 @@ dsh plugin --profile web add dsh-slack
|
|
|
30
36
|
| 键 | 类型 | 必填 | 说明 |
|
|
31
37
|
| --- | --- | --- | --- |
|
|
32
38
|
| `token` | string | 是* | Slack 令牌:机器人令牌(`xoxb-`)或用户令牌(`xoxp-`)。留空时回退到环境变量 `DSH_SLACK_TOKEN`。 |
|
|
39
|
+
| `appToken` | string | 否* | App-Level Token(`xapp-` 开头),开启 Socket Mode 后用于接收消息。留空时回退到环境变量 `DSH_SLACK_APP_TOKEN`。 |
|
|
33
40
|
| `defaultChannel` | string | 否 | 默认频道(如 `#general`)。模型未特别指定频道时的默认目标,写进 `channel` 参数说明里。 |
|
|
34
41
|
|
|
35
|
-
`*` `token`
|
|
36
|
-
|
|
42
|
+
`*` `token` 在「配置层」可留空,此时回退环境变量;两者都为空时插件照常加载,但调用发消息/列频道/
|
|
43
|
+
回复工具时会返回中文报错。`appToken` 留空则只告警、不崩溃,`slack_inbox` 返回空队列(单向模式)。
|
|
37
44
|
|
|
38
45
|
**方式一:环境变量(推荐,不写死令牌)**
|
|
39
46
|
|
|
40
47
|
```sh
|
|
41
48
|
# 在启动 dsh 的进程里设置
|
|
42
49
|
export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
50
|
+
export DSH_SLACK_APP_TOKEN=xapp-你的App级令牌
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
**方式二:profile 的 cordis.patch.yml 直接覆盖**
|
|
@@ -51,10 +59,12 @@ export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
|
51
59
|
- id: slack
|
|
52
60
|
config:
|
|
53
61
|
token: 'xoxb-你的机器人令牌'
|
|
62
|
+
appToken: 'xapp-你的App级令牌'
|
|
54
63
|
defaultChannel: '#general'
|
|
55
64
|
```
|
|
56
65
|
|
|
57
|
-
> 优先级:`config.token` > 环境变量 `DSH_SLACK_TOKEN
|
|
66
|
+
> 优先级:`config.token` > 环境变量 `DSH_SLACK_TOKEN`;`config.appToken` > 环境变量
|
|
67
|
+
> `DSH_SLACK_APP_TOKEN`。
|
|
58
68
|
|
|
59
69
|
### 创建 Slack App 并拿令牌
|
|
60
70
|
|
|
@@ -66,6 +76,23 @@ export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
|
66
76
|
4. 拿到 **Bot User OAuth Token**(以 `xoxb-` 开头)。
|
|
67
77
|
5. 把机器人(App)加进它要发消息的频道:在频道里 `/invite @你的机器人`(私有频道必需)。
|
|
68
78
|
|
|
79
|
+
### 开启 Socket Mode(双向收消息)
|
|
80
|
+
|
|
81
|
+
要接收 Slack 消息(`slack_inbox` / `slack_reply`),需要开启 Socket Mode 并生成 App-Level Token:
|
|
82
|
+
|
|
83
|
+
1. 打开 <https://api.slack.com/apps>,进入你的 App。
|
|
84
|
+
2. 左侧 **Socket Mode** 页 → 打开开关(**Enable Socket Mode**)。
|
|
85
|
+
3. 点 **Generate Token and Scopes** 生成 App-Level Token:给 Token 起名,勾选 `connections:write` scope。
|
|
86
|
+
生成后拿到以 `xapp-` 开头的 App-Level Token(只显示一次,请立即保存)。
|
|
87
|
+
4. 进入 **Event Subscriptions** 页,开启事件订阅,在 **Subscribe to bot events → Add Bot User Event** 添加:
|
|
88
|
+
- `message.channels`(公开频道消息)
|
|
89
|
+
- `message.im`(机器人私信)
|
|
90
|
+
5. 把 App-Level Token 配进 `appToken`(或环境变量 `DSH_SLACK_APP_TOKEN`)。
|
|
91
|
+
6. 重启 dsh Web 服务,插件自动通过 Socket Mode 建立连接并开始收消息。
|
|
92
|
+
|
|
93
|
+
> 未配置 `appToken` 时插件**不会崩溃**:只打印告警,`slack_inbox` 返回空队列(中文提示)。
|
|
94
|
+
> Socket Mode 网络错误由 SDK 自动重连,插件只记录告警、不抛崩。
|
|
95
|
+
|
|
69
96
|
## 工具清单
|
|
70
97
|
|
|
71
98
|
### `slack_notify`
|
|
@@ -76,7 +103,7 @@ export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
|
76
103
|
| --- | --- | --- | --- |
|
|
77
104
|
| `channel` | string | 是 | 频道名(如 `#general`)或频道 ID。 |
|
|
78
105
|
| `text` | string | 是 | 要发送的 Markdown 文本。 |
|
|
79
|
-
| `thread_ts` | string | 否 | 回复某条消息所在线程的 `ts
|
|
106
|
+
| `thread_ts` | string | 否 | 回复某条消息所在线程的 `ts`。 |
|
|
80
107
|
|
|
81
108
|
返回:`{ "ts": "...", "channel": "#general" }`(`ts` 供后续 `thread_ts` 引用)。
|
|
82
109
|
|
|
@@ -86,11 +113,35 @@ export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
|
86
113
|
|
|
87
114
|
无参数。返回:`{ "channels": [{ "id": "...", "name": "..." }, ...] }`。
|
|
88
115
|
|
|
116
|
+
### `slack_inbox`
|
|
117
|
+
|
|
118
|
+
读取通过 Socket Mode 收到的消息(内存队列,最多保留 200 条,新的在前)。
|
|
119
|
+
|
|
120
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `limit` | integer | 否 | 最多返回的消息数(默认 10,范围 1-50)。 |
|
|
123
|
+
| `markRead` | boolean | 否 | 为 `true` 时,返回后清空收件箱队列(标记已读)。 |
|
|
124
|
+
|
|
125
|
+
返回:`{ "messages": [{ "ts": "...", "channel": "...", "user": "...", "text": "..." }, ...] }`。
|
|
126
|
+
|
|
127
|
+
### `slack_reply`
|
|
128
|
+
|
|
129
|
+
以线程回复形式回复某条收件箱消息(底层 `chat.postMessage` 带 `thread_ts`)。
|
|
130
|
+
|
|
131
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `channel` | string | 是 | 频道名(如 `#general`)或频道 ID。 |
|
|
134
|
+
| `text` | string | 是 | 回复内容(Markdown 文本)。 |
|
|
135
|
+
| `thread_ts` | string | 是 | 要回复消息的 `ts`(来自 `slack_inbox` 返回的 `ts`)。 |
|
|
136
|
+
|
|
137
|
+
返回:`{ "ts": "...", "channel": "#general" }`。
|
|
138
|
+
|
|
89
139
|
## 错误处理
|
|
90
140
|
|
|
91
141
|
所有错误信息均为中文,模型与用户都能直接读懂:
|
|
92
142
|
|
|
93
143
|
- 未配置令牌:`token 未配置:缺少 Slack 机器人令牌(xoxb-)…请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。`
|
|
144
|
+
- 未配置 App-Level Token:只告警,`slack_inbox` 返回空队列(含中文提示),不影响其它工具。
|
|
94
145
|
- `invalid_auth`:提示检查/重新生成 token。
|
|
95
146
|
- `channel_not_found`:提示频道名或频道 ID 错误。
|
|
96
147
|
- `not_in_channel`:提示先把机器人 App 邀请进频道。
|
|
@@ -104,23 +155,24 @@ pnpm build # tsc 编译 src → lib
|
|
|
104
155
|
pnpm test # 先 build,再用 node:test 跑 test/*.test.mjs
|
|
105
156
|
```
|
|
106
157
|
|
|
107
|
-
测试无需真实 token:参数编译、配置解析(含 env 回退)、工具注册(
|
|
108
|
-
注入 fake client 断言 `postMessage`
|
|
158
|
+
测试无需真实 token:参数编译、配置解析(含 env 回退)、工具注册(4 个工具 + 缺配置中文报错)、
|
|
159
|
+
注入 fake client 断言 `postMessage` 参数(含 `thread_ts`)、输出 schema 纯 JSON 校验、
|
|
160
|
+
收件箱队列容量/清空、Socket Mode 事件解析(fake event 对象)、appToken 缺失不崩。
|
|
109
161
|
|
|
110
162
|
## 已知限制与路线图
|
|
111
163
|
|
|
112
|
-
- **v0.1 是单向通知(agent→Slack
|
|
113
|
-
- **不支持**
|
|
164
|
+
- **v0.1 是单向通知(agent→Slack)**;**v0.2 通过 Socket Mode 实现双向**(`slack_inbox` / `slack_reply`)。
|
|
165
|
+
- **不支持** RTM、交互组件(按钮/弹窗/slash command 回复)。
|
|
166
|
+
- `slack_inbox` 为进程内存队列:重启即清空,且不持久化;最多保留 200 条,满则丢最旧。
|
|
114
167
|
- `channel` 为必填参数,`defaultChannel` 目前只作为说明提示写入 `channel` 参数描述,不替代
|
|
115
168
|
必填的 `channel`。
|
|
116
|
-
-
|
|
169
|
+
- 令牌缺失时插件仍会加载(懒加载),错误在工具被调用时才抛出;appToken 缺失则仅告警。
|
|
117
170
|
|
|
118
|
-
路线图:v0.
|
|
119
|
-
交互组件。
|
|
171
|
+
路线图:v0.3 计划引入交互组件(按钮/弹窗)与持久化收件箱。
|
|
120
172
|
|
|
121
173
|
## 依赖
|
|
122
174
|
|
|
123
|
-
- 运行时:`@slack/web-api`(官方 WebClient
|
|
175
|
+
- 运行时:`@slack/web-api`(官方 WebClient)、`@slack/socket-mode`(Socket Mode 客户端)
|
|
124
176
|
- peer(由宿主提供,插件运行时不直接 import):`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`
|
|
125
177
|
|
|
126
178
|
## License
|
package/cordis.patch.yml
CHANGED
package/lib/config.d.ts
CHANGED
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
export interface SlackConfig {
|
|
9
9
|
token: string;
|
|
10
10
|
defaultChannel: string;
|
|
11
|
+
/** App-Level Token(xapp- 开头),用于 Socket Mode 接收消息。 */
|
|
12
|
+
appToken: string;
|
|
13
|
+
/** Slack Web API 基地址;默认官方 https://slack.com/api/。企业网格(Enterprise Grid)与本地协议级测试可覆盖。 */
|
|
14
|
+
slackApiUrl?: string;
|
|
11
15
|
}
|
|
12
16
|
/** token 的环境变量名。 */
|
|
13
17
|
export declare const ENV_TOKEN = "DSH_SLACK_TOKEN";
|
|
18
|
+
/** appToken 的环境变量名。 */
|
|
19
|
+
export declare const ENV_APP_TOKEN = "DSH_SLACK_APP_TOKEN";
|
|
14
20
|
/**
|
|
15
21
|
* 解析原始配置。永不抛错:非法形状返回 undefined,让插件懒加载不失败。
|
|
16
22
|
*/
|
|
@@ -19,6 +25,10 @@ export declare function parseConfig(raw: unknown): SlackConfig | undefined;
|
|
|
19
25
|
* 解析 token:config.token 优先,其次环境变量 DSH_SLACK_TOKEN。
|
|
20
26
|
*/
|
|
21
27
|
export declare function resolveToken(config: SlackConfig | undefined): string;
|
|
28
|
+
/**
|
|
29
|
+
* 解析 appToken:config.appToken 优先,其次环境变量 DSH_SLACK_APP_TOKEN。
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveAppToken(config: SlackConfig | undefined): string;
|
|
22
32
|
/**
|
|
23
33
|
* 解析默认频道。
|
|
24
34
|
*/
|
package/lib/config.js
CHANGED
|
@@ -6,18 +6,22 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/** token 的环境变量名。 */
|
|
8
8
|
export const ENV_TOKEN = 'DSH_SLACK_TOKEN';
|
|
9
|
+
/** appToken 的环境变量名。 */
|
|
10
|
+
export const ENV_APP_TOKEN = 'DSH_SLACK_APP_TOKEN';
|
|
9
11
|
/**
|
|
10
12
|
* 解析原始配置。永不抛错:非法形状返回 undefined,让插件懒加载不失败。
|
|
11
13
|
*/
|
|
12
14
|
export function parseConfig(raw) {
|
|
13
15
|
if (raw === undefined || raw === null)
|
|
14
|
-
return { token: '', defaultChannel: '' };
|
|
16
|
+
return { token: '', defaultChannel: '', appToken: '' };
|
|
15
17
|
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
16
18
|
return undefined;
|
|
17
19
|
const obj = raw;
|
|
18
20
|
const token = typeof obj.token === 'string' ? obj.token.trim() : '';
|
|
19
21
|
const defaultChannel = typeof obj.defaultChannel === 'string' ? obj.defaultChannel.trim() : '';
|
|
20
|
-
|
|
22
|
+
const appToken = typeof obj.appToken === 'string' ? obj.appToken.trim() : '';
|
|
23
|
+
const slackApiUrl = typeof obj.slackApiUrl === 'string' && obj.slackApiUrl.trim() !== '' ? obj.slackApiUrl.trim() : undefined;
|
|
24
|
+
return { token, defaultChannel, appToken, ...(slackApiUrl !== undefined ? { slackApiUrl } : {}) };
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* 解析 token:config.token 优先,其次环境变量 DSH_SLACK_TOKEN。
|
|
@@ -28,6 +32,15 @@ export function resolveToken(config) {
|
|
|
28
32
|
return fromConfig;
|
|
29
33
|
return process.env[ENV_TOKEN]?.trim() ?? '';
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* 解析 appToken:config.appToken 优先,其次环境变量 DSH_SLACK_APP_TOKEN。
|
|
37
|
+
*/
|
|
38
|
+
export function resolveAppToken(config) {
|
|
39
|
+
const fromConfig = config?.appToken?.trim() ?? '';
|
|
40
|
+
if (fromConfig)
|
|
41
|
+
return fromConfig;
|
|
42
|
+
return process.env[ENV_APP_TOKEN]?.trim() ?? '';
|
|
43
|
+
}
|
|
31
44
|
/**
|
|
32
45
|
* 解析默认频道。
|
|
33
46
|
*/
|
package/lib/inbox.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket Mode 收件箱:把收到的 Slack message 事件解析进内存队列。
|
|
3
|
+
* 队列容量 200,满则丢最旧;解析为纯函数,便于注入 fake 事件测试。
|
|
4
|
+
*/
|
|
5
|
+
/** 收件箱中的一条消息。 */
|
|
6
|
+
export interface InboxMessage {
|
|
7
|
+
ts: string;
|
|
8
|
+
channel: string;
|
|
9
|
+
user: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
/** 队列容量上限。 */
|
|
13
|
+
export declare const INBOX_CAPACITY = 200;
|
|
14
|
+
/** 创建收件箱队列。 */
|
|
15
|
+
export declare function createInboxQueue(capacity?: number): InboxQueue;
|
|
16
|
+
/**
|
|
17
|
+
* 内存消息队列:先进先出,容量满时丢弃最旧消息。
|
|
18
|
+
*/
|
|
19
|
+
export declare class InboxQueue {
|
|
20
|
+
private items;
|
|
21
|
+
readonly capacity: number;
|
|
22
|
+
constructor(capacity?: number);
|
|
23
|
+
/** 追加一条消息;超过容量时丢弃最旧。 */
|
|
24
|
+
push(message: InboxMessage): void;
|
|
25
|
+
/** 返回最近 limit 条消息(新的在前)。 */
|
|
26
|
+
list(limit: number): InboxMessage[];
|
|
27
|
+
/** 清空队列。 */
|
|
28
|
+
clear(): void;
|
|
29
|
+
/** 当前队列长度。 */
|
|
30
|
+
get size(): number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 从 Socket Mode 事件里解析出一条消息;不满足条件返回 null。
|
|
34
|
+
* - 兼容直接 message 事件、slack_event 包({ type: 'events_api', body: { event } })与
|
|
35
|
+
* 按类型监听包({ event })。
|
|
36
|
+
* - 排除 subtype 非空的消息、bot 自己发的消息(bot_id 非空,或 user 等于 botUserId)。
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseMessageEvent(raw: unknown, botUserId?: string): InboxMessage | null;
|
package/lib/inbox.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket Mode 收件箱:把收到的 Slack message 事件解析进内存队列。
|
|
3
|
+
* 队列容量 200,满则丢最旧;解析为纯函数,便于注入 fake 事件测试。
|
|
4
|
+
*/
|
|
5
|
+
/** 队列容量上限。 */
|
|
6
|
+
export const INBOX_CAPACITY = 200;
|
|
7
|
+
/** 创建收件箱队列。 */
|
|
8
|
+
export function createInboxQueue(capacity = INBOX_CAPACITY) {
|
|
9
|
+
return new InboxQueue(capacity);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 内存消息队列:先进先出,容量满时丢弃最旧消息。
|
|
13
|
+
*/
|
|
14
|
+
export class InboxQueue {
|
|
15
|
+
items = [];
|
|
16
|
+
capacity;
|
|
17
|
+
constructor(capacity = INBOX_CAPACITY) {
|
|
18
|
+
this.capacity = capacity;
|
|
19
|
+
}
|
|
20
|
+
/** 追加一条消息;超过容量时丢弃最旧。 */
|
|
21
|
+
push(message) {
|
|
22
|
+
this.items.push(message);
|
|
23
|
+
if (this.items.length > this.capacity) {
|
|
24
|
+
this.items.splice(0, this.items.length - this.capacity);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** 返回最近 limit 条消息(新的在前)。 */
|
|
28
|
+
list(limit) {
|
|
29
|
+
const n = Math.max(0, Math.trunc(limit));
|
|
30
|
+
if (n === 0)
|
|
31
|
+
return [];
|
|
32
|
+
return this.items.slice(-n).reverse();
|
|
33
|
+
}
|
|
34
|
+
/** 清空队列。 */
|
|
35
|
+
clear() {
|
|
36
|
+
this.items = [];
|
|
37
|
+
}
|
|
38
|
+
/** 当前队列长度。 */
|
|
39
|
+
get size() {
|
|
40
|
+
return this.items.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isObject(value) {
|
|
44
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 从 Socket Mode 事件里解析出一条消息;不满足条件返回 null。
|
|
48
|
+
* - 兼容直接 message 事件、slack_event 包({ type: 'events_api', body: { event } })与
|
|
49
|
+
* 按类型监听包({ event })。
|
|
50
|
+
* - 排除 subtype 非空的消息、bot 自己发的消息(bot_id 非空,或 user 等于 botUserId)。
|
|
51
|
+
*/
|
|
52
|
+
export function parseMessageEvent(raw, botUserId) {
|
|
53
|
+
const event = unwrapMessageEvent(raw);
|
|
54
|
+
if (event === null)
|
|
55
|
+
return null;
|
|
56
|
+
if (event.type !== 'message')
|
|
57
|
+
return null;
|
|
58
|
+
const subtype = event.subtype;
|
|
59
|
+
if (typeof subtype === 'string' && subtype !== '')
|
|
60
|
+
return null;
|
|
61
|
+
const botId = event.bot_id;
|
|
62
|
+
if (typeof botId === 'string' && botId !== '')
|
|
63
|
+
return null;
|
|
64
|
+
if (botUserId !== undefined && event.user === botUserId)
|
|
65
|
+
return null;
|
|
66
|
+
const ts = event.ts;
|
|
67
|
+
const channel = event.channel;
|
|
68
|
+
const user = event.user;
|
|
69
|
+
const text = event.text;
|
|
70
|
+
if (typeof ts !== 'string' || ts === '')
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof channel !== 'string' || channel === '')
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof user !== 'string' || user === '')
|
|
75
|
+
return null;
|
|
76
|
+
if (typeof text !== 'string' || text === '')
|
|
77
|
+
return null;
|
|
78
|
+
return { ts, channel, user, text };
|
|
79
|
+
}
|
|
80
|
+
function unwrapMessageEvent(raw) {
|
|
81
|
+
if (!isObject(raw))
|
|
82
|
+
return null;
|
|
83
|
+
const type = raw.type;
|
|
84
|
+
if (type === 'message')
|
|
85
|
+
return raw;
|
|
86
|
+
if (type === 'events_api') {
|
|
87
|
+
const body = raw.body;
|
|
88
|
+
if (isObject(body) && isObject(body.event))
|
|
89
|
+
return body.event;
|
|
90
|
+
if (isObject(raw.event))
|
|
91
|
+
return raw.event;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// 按类型监听的包:{ event: { type: 'message', ... } }
|
|
95
|
+
const inner = raw.event;
|
|
96
|
+
if (isObject(inner) && inner.type === 'message')
|
|
97
|
+
return inner;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-slack:DSH 社区 Slack 通知/桥接插件。
|
|
3
|
-
* v0.
|
|
3
|
+
* v0.2 新增 Socket Mode 双向:slack_inbox 收消息、slack_reply 线程回复。
|
|
4
4
|
* @module dsh-slack
|
|
5
5
|
*/
|
|
6
6
|
import type { SlackContext } from './types.js';
|
|
7
7
|
export declare const name = "dsh-slack";
|
|
8
8
|
export declare const inject: string[];
|
|
9
9
|
export { compileParameters, type ParameterDsl, type ParameterDslMap, type JsonSchemaNode } from './compile.js';
|
|
10
|
-
export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, ENV_TOKEN, type SlackConfig, } from './config.js';
|
|
11
|
-
export { createWebSlackClient, mapSlackError, assertChannel, assertText, type SlackClient, type PostMessageParams, type ChannelInfo, } from './slack-client.js';
|
|
12
|
-
export { buildNotifyTool, buildChannelsTool, type ToolDeps } from './tools.js';
|
|
10
|
+
export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, resolveAppToken, ENV_TOKEN, ENV_APP_TOKEN, type SlackConfig, } from './config.js';
|
|
11
|
+
export { createWebSlackClient, mapSlackError, assertChannel, assertText, assertThreadTs, type SlackClient, type PostMessageParams, type ChannelInfo, } from './slack-client.js';
|
|
12
|
+
export { buildNotifyTool, buildChannelsTool, buildInboxTool, buildReplyTool, type ToolDeps } from './tools.js';
|
|
13
|
+
export { parseMessageEvent, InboxQueue, createInboxQueue, INBOX_CAPACITY, type InboxMessage } from './inbox.js';
|
|
14
|
+
export { startSocketModeClient, type SocketMessageHandler } from './socket.js';
|
|
13
15
|
export type { SlackContext, ToolDefinition, ToolOutputDefinition, ContentBlock } from './types.js';
|
|
14
16
|
/**
|
|
15
17
|
* 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
|
|
16
|
-
* token 缺失在每个工具的 execute
|
|
18
|
+
* token 缺失在每个工具的 execute 时抛出带中文指引的错误;
|
|
19
|
+
* appToken 存在时启动 Socket Mode(ctx.effect 管理生命周期,dispose 时断开)。
|
|
17
20
|
*/
|
|
18
21
|
export declare function apply(ctx: SlackContext, rawConfig: unknown): void;
|
package/lib/index.js
CHANGED
|
@@ -1,47 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-slack:DSH 社区 Slack 通知/桥接插件。
|
|
3
|
-
* v0.
|
|
3
|
+
* v0.2 新增 Socket Mode 双向:slack_inbox 收消息、slack_reply 线程回复。
|
|
4
4
|
* @module dsh-slack
|
|
5
5
|
*/
|
|
6
|
-
import { parseConfig, requireToken } from './config.js';
|
|
6
|
+
import { parseConfig, requireToken, resolveAppToken, resolveToken } from './config.js';
|
|
7
7
|
import { createWebSlackClient } from './slack-client.js';
|
|
8
|
-
import { buildChannelsTool, buildNotifyTool } from './tools.js';
|
|
8
|
+
import { buildChannelsTool, buildInboxTool, buildNotifyTool, buildReplyTool } from './tools.js';
|
|
9
|
+
import { createInboxQueue } from './inbox.js';
|
|
10
|
+
import { startSocketModeClient } from './socket.js';
|
|
9
11
|
export const name = 'dsh-slack';
|
|
10
12
|
export const inject = ['tools'];
|
|
11
13
|
export { compileParameters } from './compile.js';
|
|
12
|
-
export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, ENV_TOKEN, } from './config.js';
|
|
13
|
-
export { createWebSlackClient, mapSlackError, assertChannel, assertText, } from './slack-client.js';
|
|
14
|
-
export { buildNotifyTool, buildChannelsTool } from './tools.js';
|
|
14
|
+
export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, resolveAppToken, ENV_TOKEN, ENV_APP_TOKEN, } from './config.js';
|
|
15
|
+
export { createWebSlackClient, mapSlackError, assertChannel, assertText, assertThreadTs, } from './slack-client.js';
|
|
16
|
+
export { buildNotifyTool, buildChannelsTool, buildInboxTool, buildReplyTool } from './tools.js';
|
|
17
|
+
export { parseMessageEvent, InboxQueue, createInboxQueue, INBOX_CAPACITY } from './inbox.js';
|
|
18
|
+
export { startSocketModeClient } from './socket.js';
|
|
15
19
|
/**
|
|
16
20
|
* 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
|
|
17
|
-
* token 缺失在每个工具的 execute
|
|
21
|
+
* token 缺失在每个工具的 execute 时抛出带中文指引的错误;
|
|
22
|
+
* appToken 存在时启动 Socket Mode(ctx.effect 管理生命周期,dispose 时断开)。
|
|
18
23
|
*/
|
|
19
24
|
export function apply(ctx, rawConfig) {
|
|
25
|
+
const warn = (message, ...args) => {
|
|
26
|
+
const logger = ctx.logger;
|
|
27
|
+
if (logger !== undefined && typeof logger.warn === 'function') {
|
|
28
|
+
logger.warn(message, ...args);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.warn(message, ...args);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
20
34
|
let config;
|
|
21
35
|
try {
|
|
22
36
|
config = parseConfig(rawConfig);
|
|
23
37
|
}
|
|
24
38
|
catch (error) {
|
|
25
|
-
|
|
39
|
+
warn('[dsh-slack] 配置解析失败,工具将在调用时报错:', error);
|
|
26
40
|
config = undefined;
|
|
27
41
|
}
|
|
28
42
|
if (config === undefined) {
|
|
29
|
-
|
|
43
|
+
warn('[dsh-slack] 配置缺失或格式错误:slack_notify / slack_channels / slack_inbox / slack_reply 已注册,但调用时会报错。请在 profile 的 cordis.patch.yml 配置 slack 行并重启。');
|
|
30
44
|
}
|
|
31
45
|
else {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
if (!resolveToken(config)) {
|
|
47
|
+
warn('[dsh-slack] 未检测到 Slack 令牌(config.token 与 DSH_SLACK_TOKEN 均为空):slack_notify / slack_channels / slack_reply 调用时会报错。');
|
|
48
|
+
}
|
|
49
|
+
const appToken = resolveAppToken(config);
|
|
50
|
+
if (!appToken) {
|
|
51
|
+
warn('[dsh-slack] 未检测到 App-Level Token(config.appToken 与 DSH_SLACK_APP_TOKEN 均为空):未开启 Socket Mode,slack_inbox 将返回空队列。');
|
|
52
|
+
}
|
|
53
|
+
else if (!appToken.startsWith('xapp-')) {
|
|
54
|
+
warn('[dsh-slack] appToken 格式错误(应以 xapp- 开头):已跳过 Socket Mode 启动。');
|
|
35
55
|
}
|
|
36
56
|
}
|
|
57
|
+
const inboxQueue = createInboxQueue();
|
|
37
58
|
const configProvider = () => config;
|
|
38
59
|
const clientProvider = () => {
|
|
39
60
|
// 每次调用时重新解析配置(config.token 优先,环境变量回退),并懒创建客户端。
|
|
40
61
|
const current = parseConfig(rawConfig);
|
|
41
62
|
const token = requireToken(current);
|
|
42
|
-
return createWebSlackClient(token);
|
|
63
|
+
return createWebSlackClient(token, current?.slackApiUrl);
|
|
43
64
|
};
|
|
44
|
-
const
|
|
65
|
+
const inboxProvider = () => inboxQueue;
|
|
66
|
+
const deps = { clientProvider, configProvider, inboxProvider };
|
|
67
|
+
const appToken = resolveAppToken(config);
|
|
68
|
+
if (appToken !== '' && appToken.startsWith('xapp-')) {
|
|
69
|
+
const startSocket = () => startSocketModeClient(appToken, {
|
|
70
|
+
onMessage(message) {
|
|
71
|
+
inboxQueue.push(message);
|
|
72
|
+
},
|
|
73
|
+
}, warn);
|
|
74
|
+
if (typeof ctx.effect === 'function') {
|
|
75
|
+
ctx.effect(startSocket);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
startSocket();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
45
81
|
ctx.tools.register(buildNotifyTool(deps));
|
|
46
82
|
ctx.tools.register(buildChannelsTool(deps));
|
|
83
|
+
ctx.tools.register(buildInboxTool(deps));
|
|
84
|
+
ctx.tools.register(buildReplyTool(deps));
|
|
47
85
|
}
|
package/lib/slack-client.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface SlackClient {
|
|
|
23
23
|
listChannels(): Promise<ChannelInfo[]>;
|
|
24
24
|
}
|
|
25
25
|
/** 用官方 WebClient 实现。 */
|
|
26
|
-
export declare function createWebSlackClient(token: string): SlackClient;
|
|
26
|
+
export declare function createWebSlackClient(token: string, slackApiUrl?: string): SlackClient;
|
|
27
27
|
/**
|
|
28
28
|
* 把 Slack API 错误映射为全中文提示。
|
|
29
29
|
*/
|
|
@@ -32,3 +32,5 @@ export declare function mapSlackError(error: unknown): string;
|
|
|
32
32
|
export declare function assertChannel(channel: unknown): string;
|
|
33
33
|
/** 校验 text 参数:必须是非空字符串。 */
|
|
34
34
|
export declare function assertText(text: unknown): string;
|
|
35
|
+
/** 校验 thread_ts 参数:必须是待回复消息 ts 的非空字符串。 */
|
|
36
|
+
export declare function assertThreadTs(thread_ts: unknown): string;
|
package/lib/slack-client.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { WebClient } from '@slack/web-api';
|
|
6
6
|
/** 用官方 WebClient 实现。 */
|
|
7
|
-
export function createWebSlackClient(token) {
|
|
8
|
-
const client = new WebClient(token);
|
|
7
|
+
export function createWebSlackClient(token, slackApiUrl) {
|
|
8
|
+
const client = new WebClient(token, { ...(slackApiUrl !== undefined && slackApiUrl !== '' ? { slackApiUrl } : {}) });
|
|
9
9
|
return {
|
|
10
10
|
async postMessage(params) {
|
|
11
11
|
const result = await client.chat.postMessage({
|
|
@@ -63,3 +63,10 @@ export function assertText(text) {
|
|
|
63
63
|
}
|
|
64
64
|
return text;
|
|
65
65
|
}
|
|
66
|
+
/** 校验 thread_ts 参数:必须是待回复消息 ts 的非空字符串。 */
|
|
67
|
+
export function assertThreadTs(thread_ts) {
|
|
68
|
+
if (typeof thread_ts !== 'string' || thread_ts.trim() === '') {
|
|
69
|
+
throw new Error('thread_ts 参数错误:必须是待回复消息的 ts(来自 slack_inbox 返回的 ts)的非空字符串。');
|
|
70
|
+
}
|
|
71
|
+
return thread_ts.trim();
|
|
72
|
+
}
|
package/lib/socket.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket Mode 启动/生命周期:用 @slack/socket-mode 建立 WebSocket,
|
|
3
|
+
* 收到 message 事件写入收件箱;网络错误只 warn 不抛崩(SDK 自带自动重连)。
|
|
4
|
+
*/
|
|
5
|
+
import { type InboxMessage } from './inbox.js';
|
|
6
|
+
/** Socket 消息回调。 */
|
|
7
|
+
export interface SocketMessageHandler {
|
|
8
|
+
onMessage(message: InboxMessage): void;
|
|
9
|
+
}
|
|
10
|
+
type Warn = (message: string, ...args: unknown[]) => void;
|
|
11
|
+
/**
|
|
12
|
+
* 启动 Socket Mode 客户端,返回 dispose 函数(断开连接)。
|
|
13
|
+
* 网络错误 / 解析错误均 try/catch + warn,绝不抛出。
|
|
14
|
+
*/
|
|
15
|
+
export declare function startSocketModeClient(appToken: string, handler: SocketMessageHandler, warn: Warn): () => void;
|
|
16
|
+
export {};
|
package/lib/socket.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket Mode 启动/生命周期:用 @slack/socket-mode 建立 WebSocket,
|
|
3
|
+
* 收到 message 事件写入收件箱;网络错误只 warn 不抛崩(SDK 自带自动重连)。
|
|
4
|
+
*/
|
|
5
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
6
|
+
import { parseMessageEvent } from './inbox.js';
|
|
7
|
+
/**
|
|
8
|
+
* 启动 Socket Mode 客户端,返回 dispose 函数(断开连接)。
|
|
9
|
+
* 网络错误 / 解析错误均 try/catch + warn,绝不抛出。
|
|
10
|
+
*/
|
|
11
|
+
export function startSocketModeClient(appToken, handler, warn) {
|
|
12
|
+
const client = new SocketModeClient({ appToken });
|
|
13
|
+
client.on('slack_event', (...args) => {
|
|
14
|
+
try {
|
|
15
|
+
const message = parseMessageEvent(args[0]);
|
|
16
|
+
if (message !== null)
|
|
17
|
+
handler.onMessage(message);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
warn('[dsh-slack] 解析 Socket Mode 事件失败:', error);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
client.on('error', (...args) => {
|
|
24
|
+
warn('[dsh-slack] Socket Mode 连接错误(SDK 会自动重连):', args[0]);
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
client.start().catch((error) => {
|
|
28
|
+
warn('[dsh-slack] Socket Mode 启动失败:', error);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
warn('[dsh-slack] Socket Mode 启动异常:', error);
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
try {
|
|
36
|
+
client.disconnect().catch((error) => {
|
|
37
|
+
warn('[dsh-slack] Socket Mode 断开失败:', error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
warn('[dsh-slack] Socket Mode 断开异常:', error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
package/lib/tools.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 构建两个面向模型的工具:slack_notify、slack_channels。
|
|
3
|
-
* 依赖通过 ToolDeps 注入(clientProvider / configProvider),测试可注入 fake client。
|
|
4
|
-
*/
|
|
5
1
|
import type { SlackConfig } from './config.js';
|
|
6
2
|
import type { ToolDefinition } from './types.js';
|
|
7
3
|
import type { SlackClient } from './slack-client.js';
|
|
4
|
+
import type { InboxQueue } from './inbox.js';
|
|
8
5
|
/** 工具构建的依赖注入点。 */
|
|
9
6
|
export interface ToolDeps {
|
|
10
|
-
/** 返回一个 SlackClient;token 缺失时抛中文错误。 */
|
|
11
7
|
clientProvider: () => SlackClient;
|
|
12
|
-
/** 返回当前配置(供默认频道等解析)。 */
|
|
13
8
|
configProvider: () => SlackConfig | undefined;
|
|
9
|
+
inboxProvider: () => InboxQueue;
|
|
14
10
|
}
|
|
15
|
-
/** 构建 slack_notify:向指定频道/线程发送 Markdown 文本,返回 ts。 */
|
|
16
11
|
export declare function buildNotifyTool(deps: ToolDeps): ToolDefinition;
|
|
17
|
-
/** 构建 slack_channels:列出机器人可见频道(conversations.list)。 */
|
|
18
12
|
export declare function buildChannelsTool(deps: ToolDeps): ToolDefinition;
|
|
13
|
+
export declare function buildInboxTool(deps: ToolDeps): ToolDefinition;
|
|
14
|
+
export declare function buildReplyTool(deps: ToolDeps): ToolDefinition;
|
package/lib/tools.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 构建两个面向模型的工具:slack_notify、slack_channels。
|
|
3
|
-
* 依赖通过 ToolDeps 注入(clientProvider / configProvider),测试可注入 fake client。
|
|
4
|
-
*/
|
|
5
1
|
import { compileParameters } from './compile.js';
|
|
6
2
|
import { resolveDefaultChannel } from './config.js';
|
|
7
|
-
import { assertChannel, assertText, mapSlackError } from './slack-client.js';
|
|
8
|
-
/** slack_notify 的输出 schema(原始 JSON Schema,纯 JSON 无 undefined)。 */
|
|
3
|
+
import { assertChannel, assertText, assertThreadTs, mapSlackError } from './slack-client.js';
|
|
9
4
|
const NOTIFY_OUTPUT_SCHEMA = {
|
|
10
5
|
type: 'object',
|
|
11
6
|
properties: {
|
|
@@ -15,7 +10,6 @@ const NOTIFY_OUTPUT_SCHEMA = {
|
|
|
15
10
|
required: ['ts', 'channel'],
|
|
16
11
|
additionalProperties: true,
|
|
17
12
|
};
|
|
18
|
-
/** slack_channels 的输出 schema。 */
|
|
19
13
|
const CHANNELS_OUTPUT_SCHEMA = {
|
|
20
14
|
type: 'object',
|
|
21
15
|
properties: {
|
|
@@ -35,14 +29,39 @@ const CHANNELS_OUTPUT_SCHEMA = {
|
|
|
35
29
|
required: ['channels'],
|
|
36
30
|
additionalProperties: true,
|
|
37
31
|
};
|
|
32
|
+
const INBOX_OUTPUT_SCHEMA = {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
messages: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
items: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
ts: { type: 'string' },
|
|
41
|
+
channel: { type: 'string' },
|
|
42
|
+
user: { type: 'string' },
|
|
43
|
+
text: { type: 'string' },
|
|
44
|
+
},
|
|
45
|
+
required: ['ts', 'channel', 'user', 'text'],
|
|
46
|
+
additionalProperties: true,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['messages'],
|
|
51
|
+
additionalProperties: true,
|
|
52
|
+
};
|
|
38
53
|
function textBlock(text) {
|
|
39
54
|
return { type: 'text', text };
|
|
40
55
|
}
|
|
41
|
-
|
|
56
|
+
function clampLimit(value) {
|
|
57
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
58
|
+
return 10;
|
|
59
|
+
return Math.min(50, Math.max(1, Math.trunc(value)));
|
|
60
|
+
}
|
|
42
61
|
export function buildNotifyTool(deps) {
|
|
43
62
|
const defaultChannel = resolveDefaultChannel(deps.configProvider());
|
|
44
63
|
const channelDescription = defaultChannel
|
|
45
|
-
?
|
|
64
|
+
? '目标频道:频道名(如 #general)或频道 ID。未特别指定时可使用默认频道 ' + defaultChannel + '。'
|
|
46
65
|
: '目标频道:频道名(如 #general)或频道 ID。';
|
|
47
66
|
const parameters = compileParameters({
|
|
48
67
|
channel: { type: 'string', required: true, description: channelDescription },
|
|
@@ -53,7 +72,7 @@ export function buildNotifyTool(deps) {
|
|
|
53
72
|
schema: NOTIFY_OUTPUT_SCHEMA,
|
|
54
73
|
render: (_args, value) => {
|
|
55
74
|
const v = value;
|
|
56
|
-
return [textBlock(
|
|
75
|
+
return [textBlock('已发送到 ' + v.channel + '(消息 ts:' + v.ts + ')。')];
|
|
57
76
|
},
|
|
58
77
|
};
|
|
59
78
|
return {
|
|
@@ -84,7 +103,6 @@ export function buildNotifyTool(deps) {
|
|
|
84
103
|
},
|
|
85
104
|
};
|
|
86
105
|
}
|
|
87
|
-
/** 构建 slack_channels:列出机器人可见频道(conversations.list)。 */
|
|
88
106
|
export function buildChannelsTool(deps) {
|
|
89
107
|
const parameters = compileParameters({});
|
|
90
108
|
const output = {
|
|
@@ -94,8 +112,8 @@ export function buildChannelsTool(deps) {
|
|
|
94
112
|
if (v.channels.length === 0) {
|
|
95
113
|
return [textBlock('机器人当前看不到任何频道(可能需要先把 App 加入频道)。')];
|
|
96
114
|
}
|
|
97
|
-
const names = v.channels.map((c) =>
|
|
98
|
-
return [textBlock(
|
|
115
|
+
const names = v.channels.map((c) => '#' + c.name).join('、');
|
|
116
|
+
return [textBlock('机器人可见频道(' + v.channels.length + ' 个):' + names + '。')];
|
|
99
117
|
},
|
|
100
118
|
};
|
|
101
119
|
return {
|
|
@@ -116,3 +134,71 @@ export function buildChannelsTool(deps) {
|
|
|
116
134
|
},
|
|
117
135
|
};
|
|
118
136
|
}
|
|
137
|
+
export function buildInboxTool(deps) {
|
|
138
|
+
const parameters = compileParameters({
|
|
139
|
+
limit: { type: 'integer', description: '最多返回的消息数(默认 10,范围 1-50)。' },
|
|
140
|
+
markRead: { type: 'boolean', description: '为 true 时,返回后清空收件箱队列(标记已读)。' },
|
|
141
|
+
});
|
|
142
|
+
const output = {
|
|
143
|
+
schema: INBOX_OUTPUT_SCHEMA,
|
|
144
|
+
render: (_args, value) => {
|
|
145
|
+
const v = value;
|
|
146
|
+
if (v.messages.length === 0) {
|
|
147
|
+
return [textBlock('收件箱为空:尚未开启 Socket Mode(缺少 appToken),或暂未收到新的 Slack 消息。请确认已配置 appToken 并订阅 message.channels / message.im 事件。')];
|
|
148
|
+
}
|
|
149
|
+
const lines = v.messages.map((m) => '[' + m.channel + '] ' + m.user + '(ts: ' + m.ts + '):' + m.text);
|
|
150
|
+
return [textBlock('收件箱(' + v.messages.length + ' 条,新的在前):\n' + lines.join('\n'))];
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
name: 'slack_inbox',
|
|
155
|
+
description: '读取通过 Socket Mode 收到的 Slack 消息(内存队列,最多保留 200 条,新的在前)。',
|
|
156
|
+
parameters,
|
|
157
|
+
output,
|
|
158
|
+
async execute(args) {
|
|
159
|
+
const raw = args;
|
|
160
|
+
const limit = clampLimit(raw.limit);
|
|
161
|
+
const markRead = raw.markRead === true;
|
|
162
|
+
const queue = deps.inboxProvider();
|
|
163
|
+
const messages = queue.list(limit);
|
|
164
|
+
if (markRead)
|
|
165
|
+
queue.clear();
|
|
166
|
+
return { messages: messages.map((m) => ({ ts: m.ts, channel: m.channel, user: m.user, text: m.text })) };
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function buildReplyTool(deps) {
|
|
171
|
+
const parameters = compileParameters({
|
|
172
|
+
channel: { type: 'string', required: true, description: '目标频道:频道名(如 #general)或频道 ID。' },
|
|
173
|
+
text: { type: 'string', required: true, description: '回复内容(Markdown 文本)。' },
|
|
174
|
+
thread_ts: { type: 'string', required: true, description: '要回复消息的 ts(来自 slack_inbox 返回的 ts)。' },
|
|
175
|
+
});
|
|
176
|
+
const output = {
|
|
177
|
+
schema: NOTIFY_OUTPUT_SCHEMA,
|
|
178
|
+
render: (_args, value) => {
|
|
179
|
+
const v = value;
|
|
180
|
+
return [textBlock('已线程回复 ' + v.channel + '(消息 ts:' + v.ts + ')。')];
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
name: 'slack_reply',
|
|
185
|
+
description: '以线程回复的形式回复某条 Slack 消息(chat.postMessage 带 thread_ts)。',
|
|
186
|
+
parameters,
|
|
187
|
+
output,
|
|
188
|
+
timeoutMs: 30000,
|
|
189
|
+
async execute(args) {
|
|
190
|
+
const raw = args;
|
|
191
|
+
const channel = assertChannel(raw.channel);
|
|
192
|
+
const text = assertText(raw.text);
|
|
193
|
+
const thread_ts = assertThreadTs(raw.thread_ts);
|
|
194
|
+
const client = deps.clientProvider();
|
|
195
|
+
try {
|
|
196
|
+
const result = await client.postMessage({ channel, text, thread_ts });
|
|
197
|
+
return { ts: result.ts, channel };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw new Error(mapSlackError(error));
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -26,4 +26,10 @@ export interface SlackContext {
|
|
|
26
26
|
tools: {
|
|
27
27
|
register(definition: ToolDefinition): () => void;
|
|
28
28
|
};
|
|
29
|
+
/** Cordis 生命周期:传入启动函数,其返回的清理函数在 dispose 时执行。 */
|
|
30
|
+
effect?: (start: () => void | (() => void)) => void;
|
|
31
|
+
/** 可选日志器;缺省回退 console.warn。 */
|
|
32
|
+
logger?: {
|
|
33
|
+
warn(message: string, ...args: unknown[]): void;
|
|
34
|
+
};
|
|
29
35
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-slack",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "DSH 社区插件:Slack 通知/桥接(v0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DSH 社区插件:Slack 通知/桥接(v0.2 支持 Socket Mode 双向:收件箱 + 线程回复)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"build": "tsc -p tsconfig.json",
|
|
29
29
|
"prepare": "tsc -p tsconfig.json",
|
|
30
30
|
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
31
|
-
"prepublishOnly": "pnpm run build"
|
|
31
|
+
"prepublishOnly": "pnpm run build",
|
|
32
|
+
"test:protocol": "node test/protocol-acceptance.mjs"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
35
|
+
"@slack/socket-mode": "^3.0.0",
|
|
34
36
|
"@slack/web-api": "^8.0.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
@@ -50,5 +52,6 @@
|
|
|
50
52
|
"dsh-plugin",
|
|
51
53
|
"deepseek-harness",
|
|
52
54
|
"dsh"
|
|
53
|
-
]
|
|
55
|
+
],
|
|
56
|
+
"packageManager": "pnpm@11.7.0"
|
|
54
57
|
}
|