dsh-slack 0.3.3 → 0.3.5
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 +34 -0
- package/README.en.md +24 -186
- package/README.md +22 -188
- package/docs/CHANGELOG.en.md +21 -0
- package/docs/USAGE.en.md +189 -0
- package/docs/USAGE.md +193 -0
- package/docs/VALIDATION.md +24 -0
- package/docs/validation-0.3.5.md +19 -0
- package/lib/inbox.d.ts +1 -1
- package/lib/inbox.js +4 -8
- package/lib/index.d.ts +1 -1
- package/lib/index.js +5 -3
- package/lib/slack-client.js +7 -9
- package/lib/socket.d.ts +20 -10
- package/lib/socket.js +102 -36
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +13 -8
- package/lib/types.d.ts +1 -0
- package/package.json +5 -3
package/docs/USAGE.en.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# dsh-slack usage guide
|
|
2
|
+
|
|
3
|
+
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## Connection diagnostics
|
|
6
|
+
|
|
7
|
+
Configured credentials do not prove an active inbox connection. slack_health reports connection state. Transient failures back off; invalid credentials and permanent authorization errors stop retries. Reload after correcting credentials. Sends are not retried implicitly.
|
|
8
|
+
|
|
9
|
+
## Current improvements
|
|
10
|
+
|
|
11
|
+
slack_inbox with limit=10 and markRead=true returns remaining; call again for unread messages. After a timeout or missing ts, inspect the channel before retrying to avoid duplicate notifications.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- `slack_notify`: send a Markdown text message to a channel (or thread), returning the message `ts`.
|
|
16
|
+
- `slack_channels`: list the channels currently visible to the bot (`conversations.list`, paginating through `next_cursor` until complete).
|
|
17
|
+
- `slack_inbox`: read messages received via Socket Mode (in-memory queue, keeps up to 200; deduplicates retries and consumes atomically with `markRead=true`).
|
|
18
|
+
- `slack_reply`: reply to an inbox message as a thread (`chat.postMessage` with `thread_ts`).
|
|
19
|
+
- WebClient instances are cached by `token + slackApiUrl` and rebuilt automatically when configuration changes.
|
|
20
|
+
- Configuration via `cordis.patch.yml`; tokens support environment-variable fallback (`DSH_SLACK_TOKEN` / `DSH_SLACK_APP_TOKEN`).
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
The plugin runs inside the host process, is installed into the profile via `dsh plugin`, and takes effect after a restart:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add dsh-slack
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
After installing, restart your dsh Web service; the four tools `slack_notify` / `slack_channels` / `slack_inbox` / `slack_reply` become visible to the model.
|
|
31
|
+
|
|
32
|
+
## Uninstall
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dsh plugin --profile web remove dsh-slack
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
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:
|
|
43
|
+
|
|
44
|
+
| Key | Type | Required | Description |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| `token` | string | yes* | Slack token: bot token (`xoxb-`) or user token (`xoxp-`). When empty, falls back to the `DSH_SLACK_TOKEN` env var. |
|
|
47
|
+
| `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. |
|
|
48
|
+
| `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. |
|
|
49
|
+
|
|
50
|
+
`*` `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).
|
|
51
|
+
|
|
52
|
+
**Method 1: environment variables (recommended, no hard-coded tokens)**
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
# 在启动 dsh 的进程里设置
|
|
56
|
+
export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
57
|
+
export DSH_SLACK_APP_TOKEN=xapp-你的App级令牌
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Method 2: override in the profile's cordis.patch.yml**
|
|
61
|
+
|
|
62
|
+
In your profile directory (`$DSH_HOME/profiles/web/cordis.patch.yml`) append:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
# 覆盖 dsh-slack 的 slack 行配置(整体替换)
|
|
66
|
+
- id: slack
|
|
67
|
+
config:
|
|
68
|
+
token: 'xoxb-你的机器人令牌'
|
|
69
|
+
appToken: 'xapp-你的App级令牌'
|
|
70
|
+
defaultChannel: '#general'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> Priority: `config.token` > env var `DSH_SLACK_TOKEN`; `config.appToken` > env var
|
|
74
|
+
> `DSH_SLACK_APP_TOKEN`.
|
|
75
|
+
|
|
76
|
+
### Create a Slack App and get a token
|
|
77
|
+
|
|
78
|
+
1. Open <https://api.slack.com/apps>, click **Create New App** (choose `From scratch`, name the app, select the workspace).
|
|
79
|
+
2. On the **OAuth & Permissions** page, under **Scopes → Bot Token Scopes** check:
|
|
80
|
+
- `chat:write` (required to send messages)
|
|
81
|
+
- `channels:read` (required to list channels)
|
|
82
|
+
3. Back at the top, click **Install to Workspace** (authorize).
|
|
83
|
+
4. Grab the **Bot User OAuth Token** (starts with `xoxb-`).
|
|
84
|
+
5. Add the bot (App) to the channels it should post in: `/invite @your-bot` in the channel (required for private channels).
|
|
85
|
+
|
|
86
|
+
### Enable Socket Mode (two-way message receiving)
|
|
87
|
+
|
|
88
|
+
To receive Slack messages (`slack_inbox` / `slack_reply`), enable Socket Mode and generate an App-Level Token:
|
|
89
|
+
|
|
90
|
+
1. Open <https://api.slack.com/apps> and open your App.
|
|
91
|
+
2. Open the **Socket Mode** page → turn on the toggle (**Enable Socket Mode**).
|
|
92
|
+
3. Click **Generate Token and Scopes** to create an App-Level Token: name the token, check the `connections:write` scope.
|
|
93
|
+
Copy the `xapp-` App-Level Token (shown only once — save it immediately).
|
|
94
|
+
4. Open the **Event Subscriptions** page, enable events, and under **Subscribe to bot events → Add Bot User Event** add:
|
|
95
|
+
- `message.channels` (public channel messages)
|
|
96
|
+
- `message.im` (bot DMs)
|
|
97
|
+
5. Put the App-Level Token into `appToken` (or the `DSH_SLACK_APP_TOKEN` env var).
|
|
98
|
+
6. Restart the dsh Web service; the plugin connects automatically via Socket Mode and starts receiving messages.
|
|
99
|
+
|
|
100
|
+
> Without `appToken` the plugin **won't crash**: it only prints a warning and `slack_inbox` returns an empty queue (with a Chinese hint).
|
|
101
|
+
> Socket Mode network errors are auto-reconnected by the SDK; the plugin only logs a warning and never throws.
|
|
102
|
+
|
|
103
|
+
## Tool reference
|
|
104
|
+
|
|
105
|
+
### `slack_notify`
|
|
106
|
+
|
|
107
|
+
Send Markdown text to a channel/thread (underlying `chat.postMessage`).
|
|
108
|
+
|
|
109
|
+
| Param | Type | Required | Description |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| `channel` | string | yes | Channel name (e.g. `#general`) or channel ID. |
|
|
112
|
+
| `text` | string | yes | The Markdown text to send. |
|
|
113
|
+
| `thread_ts` | string | no | The `ts` of the thread to reply to. |
|
|
114
|
+
|
|
115
|
+
Returns: `{ "ts": "...", "channel": "#general" }` (`ts` for later `thread_ts` reference).
|
|
116
|
+
|
|
117
|
+
### `slack_channels`
|
|
118
|
+
|
|
119
|
+
List channels visible to the bot (underlying `conversations.list`).
|
|
120
|
+
|
|
121
|
+
No parameters. Returns: `{ "channels": [{ "id": "...", "name": "..." }, ...] }`.
|
|
122
|
+
|
|
123
|
+
### `slack_inbox`
|
|
124
|
+
|
|
125
|
+
Read messages received via Socket Mode (in-memory queue, keeps up to 200, newest first).
|
|
126
|
+
|
|
127
|
+
| Param | Type | Required | Description |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| `limit` | integer | no | Maximum number of messages to return (default 10, range 1-50). |
|
|
130
|
+
| `markRead` | boolean | no | When `true`, clears the inbox queue after returning (mark as read). |
|
|
131
|
+
|
|
132
|
+
Returns: `{ "messages": [{ "ts": "...", "channel": "...", "user": "...", "text": "..." }, ...] }`.
|
|
133
|
+
|
|
134
|
+
### `slack_reply`
|
|
135
|
+
|
|
136
|
+
Reply to an inbox message as a thread (underlying `chat.postMessage` with `thread_ts`).
|
|
137
|
+
|
|
138
|
+
| Param | Type | Required | Description |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `channel` | string | yes | Channel name (e.g. `#general`) or channel ID. |
|
|
141
|
+
| `text` | string | yes | Reply content (Markdown text). |
|
|
142
|
+
| `thread_ts` | string | yes | The `ts` of the message to reply to (from `slack_inbox`). |
|
|
143
|
+
|
|
144
|
+
Returns: `{ "ts": "...", "channel": "#general" }`.
|
|
145
|
+
|
|
146
|
+
## Error handling
|
|
147
|
+
|
|
148
|
+
All error messages are in Chinese, readable by both the model and the user:
|
|
149
|
+
|
|
150
|
+
- Unconfigured token: `token 未配置:缺少 Slack 机器人令牌(xoxb-)…请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。`
|
|
151
|
+
- Unconfigured App-Level Token: only a warning; `slack_inbox` returns an empty queue (with a Chinese hint) and other tools are unaffected.
|
|
152
|
+
- `invalid_auth`: hints to check/regenerate the token.
|
|
153
|
+
- `channel_not_found`: hints the channel name or ID is wrong.
|
|
154
|
+
- `not_in_channel`: hints to invite the bot App into the channel first.
|
|
155
|
+
- `token_revoked` / `account_inactive` / `missing_scope` / `not_authed`: hints permission or token expiry, reinstall the App.
|
|
156
|
+
- `is_archived`: hints the channel is archived and cannot receive messages.
|
|
157
|
+
- `msg_too_long`: hints the message exceeds Slack's 40,000-character limit.
|
|
158
|
+
- `ratelimited`: hints to retry after a short wait.
|
|
159
|
+
|
|
160
|
+
## Development and testing
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
pnpm install # 安装依赖并触发 prepare(tsc 构建)
|
|
164
|
+
pnpm build # tsc 编译 src → lib
|
|
165
|
+
pnpm test # 先 build,再用 node:test 跑 test/*.test.mjs
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Tests need no real token: parameter compilation, config parsing (incl. env fallback), tool registration (4 tools + Chinese error on missing config),
|
|
169
|
+
injecting a fake client to assert `postMessage` arguments (incl. `thread_ts`), output schema pure-JSON validation,
|
|
170
|
+
inbox queue capacity/clear/deduplication/atomic drain, Socket Mode event parsing (fake event objects), and no-crash on missing appToken.
|
|
171
|
+
|
|
172
|
+
## Known limitations and roadmap
|
|
173
|
+
|
|
174
|
+
- **v0.1 is one-way notification (agent→Slack)**; **v0.2 is two-way via Socket Mode** (`slack_inbox` / `slack_reply`).
|
|
175
|
+
- **No** RTM or interactive components (buttons/modals/slash command replies).
|
|
176
|
+
- `slack_inbox` is an in-process memory queue: cleared on restart, not persisted; keeps up to 200, dropping the oldest when full.
|
|
177
|
+
- `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`.
|
|
178
|
+
- 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.
|
|
179
|
+
|
|
180
|
+
Roadmap: v0.3 plans to introduce interactive components (buttons/modals) and a persisted inbox.
|
|
181
|
+
|
|
182
|
+
## Dependencies
|
|
183
|
+
|
|
184
|
+
- Runtime: `@slack/web-api` (official WebClient), `@slack/socket-mode` (Socket Mode client)
|
|
185
|
+
- peer (provided by the host; not imported directly by the plugin at runtime): `@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# dsh-slack 使用说明
|
|
2
|
+
|
|
3
|
+
[返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## 连接诊断
|
|
6
|
+
|
|
7
|
+
凭据已填写不等于收件箱已连接。slack_health 显示真实连接状态;临时故障退避重连,凭据失效或权限错误停止重试。修正凭据后重新加载插件。发送操作不会隐式重试。
|
|
8
|
+
|
|
9
|
+
## 本次改进
|
|
10
|
+
|
|
11
|
+
slack_inbox { limit: 10, markRead: true } 返回 remaining,继续调用可查看余下消息。遇到超时或缺少 ts 时先检查频道,确认未发送后再重试,避免重复通知。
|
|
12
|
+
|
|
13
|
+
## 功能
|
|
14
|
+
|
|
15
|
+
- `slack_notify`:向指定频道(或线程)发送一条 Markdown 文本消息,返回消息 `ts`。
|
|
16
|
+
- `slack_channels`:列出机器人当前可见的频道(`conversations.list`,自动沿 `next_cursor` 翻页拉全)。
|
|
17
|
+
- `slack_inbox`:读取通过 Socket Mode 收到的消息(内存队列,最多保留 200 条;自动去重,`markRead=true` 原子消费)。
|
|
18
|
+
- `slack_reply`:以线程回复形式回复某条收件箱消息(`chat.postMessage` 带 `thread_ts`)。
|
|
19
|
+
- WebClient 按 `token + slackApiUrl` 缓存复用,配置变更时自动重建。
|
|
20
|
+
- 配置走 `cordis.patch.yml`,令牌支持环境变量回退(`DSH_SLACK_TOKEN` / `DSH_SLACK_APP_TOKEN`)。
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
插件运行在宿主进程内,通过 `dsh plugin` 安装进 profile,重启后生效:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add dsh-slack
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
安装后重启你的 dsh Web 服务,`slack_notify` / `slack_channels` / `slack_inbox` / `slack_reply`
|
|
31
|
+
四个工具即对模型可见。
|
|
32
|
+
|
|
33
|
+
## 配置
|
|
34
|
+
|
|
35
|
+
配置在 profile 的 `cordis.patch.yml` 里按 `id: slack` 覆盖本插件的行(覆盖会整体替换该行的
|
|
36
|
+
`config`,不会合并)。可用配置项:
|
|
37
|
+
|
|
38
|
+
| 键 | 类型 | 必填 | 说明 |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `token` | string | 是* | Slack 令牌:机器人令牌(`xoxb-`)或用户令牌(`xoxp-`)。留空时回退到环境变量 `DSH_SLACK_TOKEN`。 |
|
|
41
|
+
| `appToken` | string | 否* | App-Level Token(`xapp-` 开头),开启 Socket Mode 后用于接收消息。留空时回退到环境变量 `DSH_SLACK_APP_TOKEN`。 |
|
|
42
|
+
| `defaultChannel` | string | 否 | 默认频道(如 `#general`)。模型未特别指定频道时的默认目标,写进 `channel` 参数说明里。 |
|
|
43
|
+
|
|
44
|
+
`*` `token` 在「配置层」可留空,此时回退环境变量;两者都为空时插件照常加载,但调用发消息/列频道/
|
|
45
|
+
回复工具时会返回中文报错。`appToken` 留空则只告警、不崩溃,`slack_inbox` 返回空队列(单向模式)。
|
|
46
|
+
|
|
47
|
+
**方式一:环境变量(推荐,不写死令牌)**
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
# 在启动 dsh 的进程里设置
|
|
51
|
+
export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
|
|
52
|
+
export DSH_SLACK_APP_TOKEN=xapp-你的App级令牌
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**方式二:profile 的 cordis.patch.yml 直接覆盖**
|
|
56
|
+
|
|
57
|
+
在你的 profile 目录(`$DSH_HOME/profiles/web/cordis.patch.yml`)追加:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
# 覆盖 dsh-slack 的 slack 行配置(整体替换)
|
|
61
|
+
- id: slack
|
|
62
|
+
config:
|
|
63
|
+
token: 'xoxb-你的机器人令牌'
|
|
64
|
+
appToken: 'xapp-你的App级令牌'
|
|
65
|
+
defaultChannel: '#general'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> 优先级:`config.token` > 环境变量 `DSH_SLACK_TOKEN`;`config.appToken` > 环境变量
|
|
69
|
+
> `DSH_SLACK_APP_TOKEN`。
|
|
70
|
+
|
|
71
|
+
### 创建 Slack App 并拿令牌
|
|
72
|
+
|
|
73
|
+
1. 打开 <https://api.slack.com/apps>,点 **Create New App**(选 `From scratch`,给 App 起名、选工作区)。
|
|
74
|
+
2. 进入 **OAuth & Permissions** 页,在 **Scopes → Bot Token Scopes** 下勾选:
|
|
75
|
+
- `chat:write`(发消息必需)
|
|
76
|
+
- `channels:read`(列频道必需)
|
|
77
|
+
3. 回到顶部点 **Install to Workspace**(授权)。
|
|
78
|
+
4. 拿到 **Bot User OAuth Token**(以 `xoxb-` 开头)。
|
|
79
|
+
5. 把机器人(App)加进它要发消息的频道:在频道里 `/invite @你的机器人`(私有频道必需)。
|
|
80
|
+
|
|
81
|
+
### 开启 Socket Mode(双向收消息)
|
|
82
|
+
|
|
83
|
+
要接收 Slack 消息(`slack_inbox` / `slack_reply`),需要开启 Socket Mode 并生成 App-Level Token:
|
|
84
|
+
|
|
85
|
+
1. 打开 <https://api.slack.com/apps>,进入你的 App。
|
|
86
|
+
2. 左侧 **Socket Mode** 页 → 打开开关(**Enable Socket Mode**)。
|
|
87
|
+
3. 点 **Generate Token and Scopes** 生成 App-Level Token:给 Token 起名,勾选 `connections:write` scope。
|
|
88
|
+
生成后拿到以 `xapp-` 开头的 App-Level Token(只显示一次,请立即保存)。
|
|
89
|
+
4. 进入 **Event Subscriptions** 页,开启事件订阅,在 **Subscribe to bot events → Add Bot User Event** 添加:
|
|
90
|
+
- `message.channels`(公开频道消息)
|
|
91
|
+
- `message.im`(机器人私信)
|
|
92
|
+
5. 把 App-Level Token 配进 `appToken`(或环境变量 `DSH_SLACK_APP_TOKEN`)。
|
|
93
|
+
6. 重启 dsh Web 服务,插件自动通过 Socket Mode 建立连接并开始收消息。
|
|
94
|
+
|
|
95
|
+
> 未配置 `appToken` 时插件**不会崩溃**:只打印告警,`slack_inbox` 返回空队列(中文提示)。
|
|
96
|
+
> Socket Mode 网络错误由 SDK 自动重连,插件只记录告警、不抛崩。
|
|
97
|
+
|
|
98
|
+
## 卸载
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
dsh plugin --profile web remove dsh-slack
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中的对应插件行。
|
|
105
|
+
|
|
106
|
+
## 工具清单
|
|
107
|
+
|
|
108
|
+
### `slack_notify`
|
|
109
|
+
|
|
110
|
+
向指定频道/线程发送 Markdown 文本(底层 `chat.postMessage`)。
|
|
111
|
+
|
|
112
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
113
|
+
| --- | --- | --- | --- |
|
|
114
|
+
| `channel` | string | 是 | 频道名(如 `#general`)或频道 ID。 |
|
|
115
|
+
| `text` | string | 是 | 要发送的 Markdown 文本。 |
|
|
116
|
+
| `thread_ts` | string | 否 | 回复某条消息所在线程的 `ts`。 |
|
|
117
|
+
|
|
118
|
+
返回:`{ "ts": "...", "channel": "#general" }`(`ts` 供后续 `thread_ts` 引用)。
|
|
119
|
+
|
|
120
|
+
### `slack_channels`
|
|
121
|
+
|
|
122
|
+
列出机器人可见频道(底层 `conversations.list`)。
|
|
123
|
+
|
|
124
|
+
无参数。返回:`{ "channels": [{ "id": "...", "name": "..." }, ...] }`。
|
|
125
|
+
|
|
126
|
+
### `slack_inbox`
|
|
127
|
+
|
|
128
|
+
读取通过 Socket Mode 收到的消息(内存队列,最多保留 200 条,新的在前)。
|
|
129
|
+
|
|
130
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
131
|
+
| --- | --- | --- | --- |
|
|
132
|
+
| `limit` | integer | 否 | 最多返回的消息数(默认 10,范围 1-50)。 |
|
|
133
|
+
| `markRead` | boolean | 否 | 为 `true` 时,返回后清空收件箱队列(标记已读)。 |
|
|
134
|
+
|
|
135
|
+
返回:`{ "messages": [{ "ts": "...", "channel": "...", "user": "...", "text": "..." }, ...] }`。
|
|
136
|
+
|
|
137
|
+
### `slack_reply`
|
|
138
|
+
|
|
139
|
+
以线程回复形式回复某条收件箱消息(底层 `chat.postMessage` 带 `thread_ts`)。
|
|
140
|
+
|
|
141
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
142
|
+
| --- | --- | --- | --- |
|
|
143
|
+
| `channel` | string | 是 | 频道名(如 `#general`)或频道 ID。 |
|
|
144
|
+
| `text` | string | 是 | 回复内容(Markdown 文本)。 |
|
|
145
|
+
| `thread_ts` | string | 是 | 要回复消息的 `ts`(来自 `slack_inbox` 返回的 `ts`)。 |
|
|
146
|
+
|
|
147
|
+
返回:`{ "ts": "...", "channel": "#general" }`。
|
|
148
|
+
|
|
149
|
+
## 错误处理
|
|
150
|
+
|
|
151
|
+
所有错误信息均为中文,模型与用户都能直接读懂:
|
|
152
|
+
|
|
153
|
+
- 未配置令牌:`token 未配置:缺少 Slack 机器人令牌(xoxb-)…请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。`
|
|
154
|
+
- 未配置 App-Level Token:只告警,`slack_inbox` 返回空队列(含中文提示),不影响其它工具。
|
|
155
|
+
- `invalid_auth`:提示检查/重新生成 token。
|
|
156
|
+
- `channel_not_found`:提示频道名或频道 ID 错误。
|
|
157
|
+
- `not_in_channel`:提示先把机器人 App 邀请进频道。
|
|
158
|
+
- `token_revoked` / `account_inactive` / `missing_scope` / `not_authed`:提示权限或令牌失效,需重新安装 App。
|
|
159
|
+
- `is_archived`:提示频道已归档,不能发消息。
|
|
160
|
+
- `msg_too_long`:提示 Slack 单条消息超过 40,000 字符。
|
|
161
|
+
- `ratelimited`:提示请求太频繁,稍后重试。
|
|
162
|
+
|
|
163
|
+
## 开发与测试
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
pnpm install # 安装依赖并触发 prepare(tsc 构建)
|
|
167
|
+
pnpm build # tsc 编译 src → lib
|
|
168
|
+
pnpm test # 先 build,再用 node:test 跑 test/*.test.mjs
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
测试无需真实 token:参数编译、配置解析(含 env 回退)、工具注册(4 个工具 + 缺配置中文报错)、
|
|
172
|
+
注入 fake client 断言 `postMessage` 参数(含 `thread_ts`)、输出 schema 纯 JSON 校验、
|
|
173
|
+
收件箱队列容量/清空/去重/原子 drain、Socket Mode 事件解析(fake event 对象)、appToken 缺失不崩。
|
|
174
|
+
|
|
175
|
+
## 已知限制与路线图
|
|
176
|
+
|
|
177
|
+
- **v0.1 是单向通知(agent→Slack)**;**v0.2 通过 Socket Mode 实现双向**(`slack_inbox` / `slack_reply`)。
|
|
178
|
+
- **不支持** RTM、交互组件(按钮/弹窗/slash command 回复)。
|
|
179
|
+
- `slack_inbox` 为进程内存队列:重启即清空,且不持久化;最多保留 200 条,满则丢最旧。
|
|
180
|
+
- `channel` 为必填参数,`defaultChannel` 目前只作为说明提示写入 `channel` 参数描述,不替代
|
|
181
|
+
必填的 `channel`。
|
|
182
|
+
- 令牌缺失时插件仍会加载(懒加载),错误在工具被调用时才抛出;appToken 缺失则仅告警。
|
|
183
|
+
|
|
184
|
+
路线图:v0.3 计划引入交互组件(按钮/弹窗)与持久化收件箱。
|
|
185
|
+
|
|
186
|
+
## 依赖
|
|
187
|
+
|
|
188
|
+
- 运行时:`@slack/web-api`(官方 WebClient)、`@slack/socket-mode`(Socket Mode 客户端)
|
|
189
|
+
- peer(由宿主提供,插件运行时不直接 import):`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# dsh-slack 验证记录
|
|
2
|
+
|
|
3
|
+
## 2026-10-26:0.3.5
|
|
4
|
+
|
|
5
|
+
[本次修复、回归与未验收范围](validation-0.3.5.md)。
|
|
6
|
+
|
|
7
|
+
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
8
|
+
|
|
9
|
+
## 原中文记录
|
|
10
|
+
|
|
11
|
+
验证宿主:官方发布标签源码构建的 Harness `0.2.0-rc.2`(commit `639ed01539`)+ Windows / Node `24.16.0`。43 项插件测试通过;同一个宿主里 18 个插件共同加载,本插件的 5 个工具全部注册,工具 schema 与健康检查契约通过。真实 Slack 登录、Socket Mode 收件和发送尚未验收。
|
|
12
|
+
|
|
13
|
+
0.3.3 使用支持 Undici 7/8 的官方 Socket Mode SDK 3.1 或更新版本,并显式声明匹配的传输依赖,修复组合安装中旧 SDK 与宿主 Undici 8 的依赖不匹配;npm 包同时包含中英文说明。
|
|
14
|
+
|
|
15
|
+
> **v0.2 范围(双向)**:v0.1 只做「agent → Slack」单向通知;v0.2 新增 Socket Mode,
|
|
16
|
+
> 支持「Slack 消息 → agent」:`slack_inbox` 收取消息、`slack_reply` 线程回复。
|
|
17
|
+
> RTM、交互组件(按钮/弹窗/slash command 回复)不在 v0.2,见下方
|
|
18
|
+
> [已知限制与路线图](USAGE.md#已知限制与路线图)。
|
|
19
|
+
|
|
20
|
+
## Original English record
|
|
21
|
+
|
|
22
|
+
Validation host: Harness `0.2.0-rc.2` built from its official release tag (commit `639ed01539`), Windows and Node `24.16.0`. All 43 plugin tests pass; all 18 plugins mount together and this plugin registers all five tools, with tool schemas and health-check contracts passing. Real Slack authorization, Socket Mode delivery and sending have not been verified.
|
|
23
|
+
|
|
24
|
+
Version 0.3.3 requires the official Socket Mode SDK 3.1 or newer, which supports Undici 7/8, and explicitly declares its transport dependency. This fixes combined installations that paired an older SDK with the host's Undici 8. Both language versions of this README are included in the npm package.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 0.3.5 验证记录
|
|
2
|
+
|
|
3
|
+
日期:2026-10-26。官方 Windows 宿主:DSH 0.2.1-rc.3。
|
|
4
|
+
|
|
5
|
+
## 行为变化
|
|
6
|
+
|
|
7
|
+
Socket Mode 重连改为插件管理的有界退避;捕获启动与重连错误,凭据撤销等永久错误停止重试,卸载取消重连。自检区分凭据已配置、连接中、连接成功与认证失败。
|
|
8
|
+
|
|
9
|
+
## 验证与限制
|
|
10
|
+
|
|
11
|
+
- Windows 本地:49 项,49 通过、0 跳过;没有把跳过计为通过。
|
|
12
|
+
- 真实 Slack SDK 的 token_revoked 重连故障在严格未处理异常模式下复现并回归;官方 RC.3 检查未配置状态。未配置真实 Slack 账号,线上收发未验收。
|
|
13
|
+
- GitHub 的跨平台 CI 和 npm 发布完整性以该版本工作流及 registry 为准;本记录不把本地测试等同于已发布。
|
|
14
|
+
|
|
15
|
+
## English
|
|
16
|
+
|
|
17
|
+
Socket Mode uses caught, bounded reconnect backoff. Permanent authentication errors stop retries; disposal cancels reconnects. Health distinguishes configured credentials from an active connection.
|
|
18
|
+
|
|
19
|
+
Runtime/account limits are listed above. Synthetic peers and model adapters do not establish live account delivery or model quality.
|
package/lib/inbox.d.ts
CHANGED
package/lib/inbox.js
CHANGED
|
@@ -27,11 +27,11 @@ export class InboxQueue {
|
|
|
27
27
|
if (this.seen.has(key))
|
|
28
28
|
return;
|
|
29
29
|
this.seen.add(key);
|
|
30
|
+
while (this.seen.size > Math.max(200, this.capacity * 4))
|
|
31
|
+
this.seen.delete(this.seen.values().next().value);
|
|
30
32
|
this.items.push(message);
|
|
31
33
|
if (this.items.length > this.capacity) {
|
|
32
|
-
|
|
33
|
-
for (const item of removed)
|
|
34
|
-
this.seen.delete(this.key(item));
|
|
34
|
+
this.items.splice(0, this.items.length - this.capacity);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
/** 返回最近 limit 条消息(新的在前)。 */
|
|
@@ -42,24 +42,20 @@ export class InboxQueue {
|
|
|
42
42
|
return this.items.slice(-n).reverse();
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* 原子地取出最近 limit
|
|
45
|
+
* 原子地取出最近 limit 条,只将返回的消息标记为已读。
|
|
46
46
|
* 避免“先 list 再 clear”之间新到的消息被误清掉。
|
|
47
47
|
*/
|
|
48
48
|
drain(limit) {
|
|
49
49
|
const n = Math.max(0, Math.trunc(limit));
|
|
50
50
|
if (n === 0) {
|
|
51
|
-
this.clear();
|
|
52
51
|
return [];
|
|
53
52
|
}
|
|
54
53
|
const out = this.items.splice(-n).reverse();
|
|
55
|
-
this.items = [];
|
|
56
|
-
this.seen.clear();
|
|
57
54
|
return out;
|
|
58
55
|
}
|
|
59
56
|
/** 清空队列。 */
|
|
60
57
|
clear() {
|
|
61
58
|
this.items = [];
|
|
62
|
-
this.seen.clear();
|
|
63
59
|
}
|
|
64
60
|
/** 当前队列长度。 */
|
|
65
61
|
get size() {
|
package/lib/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, resolve
|
|
|
11
11
|
export { createWebSlackClient, mapSlackError, assertChannel, assertText, assertThreadTs, type SlackClient, type PostMessageParams, type ChannelInfo, } from './slack-client.js';
|
|
12
12
|
export { buildNotifyTool, buildChannelsTool, buildInboxTool, buildReplyTool, type ToolDeps } from './tools.js';
|
|
13
13
|
export { parseMessageEvent, InboxQueue, createInboxQueue, INBOX_CAPACITY, type InboxMessage } from './inbox.js';
|
|
14
|
-
export { startSocketModeClient, type SocketMessageHandler } from './socket.js';
|
|
14
|
+
export { startSocketModeClient, type SocketMessageHandler, type SocketStatus, type SocketDisposer, type SocketLifecycleOptions } from './socket.js';
|
|
15
15
|
export type { SlackContext, ToolDefinition, ToolOutputDefinition, ContentBlock } from './types.js';
|
|
16
16
|
/**
|
|
17
17
|
* 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
|
package/lib/index.js
CHANGED
|
@@ -71,10 +71,11 @@ export function apply(ctx, rawConfig) {
|
|
|
71
71
|
return client;
|
|
72
72
|
};
|
|
73
73
|
const inboxProvider = () => inboxQueue;
|
|
74
|
-
|
|
74
|
+
let socket;
|
|
75
|
+
const deps = { clientProvider, configProvider, inboxProvider, socketStatusProvider: () => socket?.status() };
|
|
75
76
|
const appToken = resolveAppToken(config);
|
|
76
77
|
if (appToken !== '' && appToken.startsWith('xapp-')) {
|
|
77
|
-
const startSocket = () => startSocketModeClient(appToken, {
|
|
78
|
+
const startSocket = () => socket = startSocketModeClient(appToken, {
|
|
78
79
|
onMessage(message) {
|
|
79
80
|
inboxQueue.push(message);
|
|
80
81
|
},
|
|
@@ -83,7 +84,8 @@ export function apply(ctx, rawConfig) {
|
|
|
83
84
|
ctx.effect(startSocket);
|
|
84
85
|
}
|
|
85
86
|
else {
|
|
86
|
-
startSocket();
|
|
87
|
+
const dispose = startSocket();
|
|
88
|
+
ctx.on?.('dispose', dispose);
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
ctx.tools.register(buildNotifyTool(deps));
|
package/lib/slack-client.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
import { WebClient } from '@slack/web-api';
|
|
6
6
|
/** 用官方 WebClient 实现。 */
|
|
7
7
|
export function createWebSlackClient(token, slackApiUrl) {
|
|
8
|
-
const client = new WebClient(token, {
|
|
8
|
+
const client = new WebClient(token, {
|
|
9
|
+
timeout: 25000, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true,
|
|
10
|
+
...(slackApiUrl !== undefined && slackApiUrl !== '' ? { slackApiUrl } : {})
|
|
11
|
+
});
|
|
9
12
|
return {
|
|
10
13
|
async postMessage(params) {
|
|
11
14
|
const result = await client.chat.postMessage({
|
|
@@ -14,6 +17,8 @@ export function createWebSlackClient(token, slackApiUrl) {
|
|
|
14
17
|
...(params.thread_ts !== undefined ? { thread_ts: params.thread_ts } : {}),
|
|
15
18
|
});
|
|
16
19
|
const ts = typeof result.ts === 'string' ? result.ts : '';
|
|
20
|
+
if (!ts)
|
|
21
|
+
throw new Error('Slack 未返回消息回执,无法确认提交结果;请先检查频道,避免直接重发。');
|
|
17
22
|
return { ts };
|
|
18
23
|
},
|
|
19
24
|
async listChannels() {
|
|
@@ -21,8 +26,7 @@ export function createWebSlackClient(token, slackApiUrl) {
|
|
|
21
26
|
let pages = 0;
|
|
22
27
|
const allChannels = [];
|
|
23
28
|
const seen = new Set();
|
|
24
|
-
//
|
|
25
|
-
// 必须沿 next_cursor 翻完,否则模型会漏掉后面的频道。
|
|
29
|
+
// Follow the official pagination cursor, with the existing 20-page cap.
|
|
26
30
|
do {
|
|
27
31
|
pages += 1;
|
|
28
32
|
if (pages > 20)
|
|
@@ -41,12 +45,6 @@ export function createWebSlackClient(token, slackApiUrl) {
|
|
|
41
45
|
cursor = typeof next === 'string' && next !== cursor ? next : '';
|
|
42
46
|
} while (cursor !== '');
|
|
43
47
|
return allChannels;
|
|
44
|
-
/*
|
|
45
|
-
// if (typeof ch.id === 'string' && typeof ch.name === 'string') {
|
|
46
|
-
// allChannels.push({ id: ch.id, name: ch.name })
|
|
47
|
-
*/
|
|
48
|
-
// }
|
|
49
|
-
// removed duplicate return
|
|
50
48
|
},
|
|
51
49
|
};
|
|
52
50
|
}
|
package/lib/socket.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* 收到 message 事件写入收件箱;网络错误只 warn 不抛崩(SDK 自带自动重连)。
|
|
4
|
-
*/
|
|
1
|
+
/** Socket Mode lifecycle: SDK auto-reconnect is disabled; every attempt is owned and caught here. */
|
|
2
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
5
3
|
import { type InboxMessage } from './inbox.js';
|
|
6
|
-
/** Socket 消息回调。 */
|
|
7
4
|
export interface SocketMessageHandler {
|
|
8
5
|
onMessage(message: InboxMessage): void;
|
|
9
6
|
}
|
|
10
7
|
type Warn = (message: string, ...args: unknown[]) => void;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
export interface SocketStatus {
|
|
9
|
+
state: 'connecting' | 'connected' | 'reconnecting' | 'auth_failed' | 'stopped';
|
|
10
|
+
attempts: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SocketClientLike {
|
|
14
|
+
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
15
|
+
start(): Promise<unknown>;
|
|
16
|
+
disconnect(): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface SocketLifecycleOptions {
|
|
19
|
+
retryBaseMs?: number;
|
|
20
|
+
createClient?: (options: ConstructorParameters<typeof SocketModeClient>[0]) => SocketClientLike;
|
|
21
|
+
}
|
|
22
|
+
export type SocketDisposer = (() => void) & {
|
|
23
|
+
status(): SocketStatus;
|
|
24
|
+
};
|
|
25
|
+
export declare function startSocketModeClient(appToken: string, handler: SocketMessageHandler, warn: Warn, options?: SocketLifecycleOptions): SocketDisposer;
|
|
16
26
|
export {};
|