dsh-hooks 0.5.0 → 0.7.0
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.md +83 -8
- package/README.zh.md +83 -8
- package/lib/client.js +727 -23
- package/lib/feishu.d.ts +6 -0
- package/lib/feishu.js +24 -3
- package/lib/index.js +1 -1
- package/lib/notify.d.ts +2 -2
- package/lib/notify.js +3 -2
- package/lib/patch-config.d.ts +47 -0
- package/lib/patch-config.js +141 -0
- package/lib/runner.d.ts +8 -0
- package/lib/runner.js +4 -1
- package/lib/server.d.ts +34 -8
- package/lib/server.js +144 -6
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -178,29 +178,104 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # end-to-end: actually r
|
|
|
178
178
|
|
|
179
179
|
After install, the dsh web settings panel gains a "Hooks" section (beside General and Plugins):
|
|
180
180
|
|
|
181
|
-
- **Status badges**: plugin version, hook count, history count
|
|
182
|
-
- **Manual tester**: pick an event (14 kinds) + reason/tool; "Simulate" shows the per-hook match report, "Execute" really triggers the matching hooks
|
|
183
|
-
- **
|
|
184
|
-
- **
|
|
181
|
+
- **Status badges**: plugin version, hook count, history count, plus live diagnostics (in-flight runs, recent failures)
|
|
182
|
+
- **Manual tester**: pick an event (14 kinds) + reason/tool; "Simulate" shows the per-hook match report, "Execute" really triggers the matching hooks; the report clears when the inputs change
|
|
183
|
+
- **Notify-channel tests**: fire a test notification at the webhook (optional Slack summary) / desktop channel and show the payload preview
|
|
184
|
+
- **Feishu connect**: scan-to-connect inside the panel — the QR code renders inline (with expiry countdown and a cancel button); after the scan the app is created, credentials + hook config are written, and the connected summary offers a one-click test card, an inline truncation-length editor (50–5000 chars, default 300, with a content preview), a re-connect flow, and a disconnect (optionally removing the Feishu hooks)
|
|
185
|
+
- **Hook list / editor**: a read-only list of the current hooks (event/when/match/run/notify + timeout/retry fields) with one-click "copy YAML"; the "edit" mode turns it into a form editor whose changes are validated (regexes, run-notify exclusivity) and written back to `cordis.patch.yml` with an automatic backup
|
|
186
|
+
- **Execution-history timeline**: at the bottom of the card, **collapsed by default** (the toggle state persists in localStorage; "expand" opens the latest 30 triggers: time / event / command / outcome / stderr tail), refreshed every 5s
|
|
185
187
|
|
|
186
188
|
CLI/headless environments are unaffected: the browser half loads only in the web GUI and the core has no UI runtime dependencies.
|
|
187
189
|
|
|
188
190
|
## Web profile HTTP routes
|
|
189
191
|
|
|
190
|
-
In the web profile (when the shared webServer service exists) dsh-hooks registers
|
|
192
|
+
In the web profile (when the shared webServer service exists) dsh-hooks registers `/dsh-hooks/*` routes, restricted to loopback by default and configurable through the environment variable below — CLI/headless environments never see them:
|
|
191
193
|
|
|
192
194
|
| Route | Method | Purpose |
|
|
193
195
|
| --- | --- | --- |
|
|
194
|
-
| `/dsh-hooks/status` | GET | plugin version, hook count, history count |
|
|
196
|
+
| `/dsh-hooks/status` | GET | plugin version, hook count, history count, the **current hook list**, and live runner stats |
|
|
195
197
|
| `/dsh-hooks/history?n=50` | GET | the latest N execution records (JSON envelope) |
|
|
196
198
|
| `/dsh-hooks/test` | POST | simulate an event: `{"event":"tool/call","tool":"ssh_exec","execute":false}` returns a per-hook match report; `execute: true` actually runs the matching hooks |
|
|
197
|
-
| `/dsh-hooks/
|
|
199
|
+
| `/dsh-hooks/notify/test` | POST | fire a test notification at a channel: `{"channel":"webhook","url":…,"slack":true}` or `{"channel":"desktop"}`; returns the payload preview |
|
|
200
|
+
| `/dsh-hooks/hooks/save` | POST | save the hook list: `{"profile":"web","hooks":[…]}` — validates (events, reasons, regexes, run-notify exclusivity), writes back to cordis.patch.yml with an automatic backup |
|
|
201
|
+
| `/dsh-hooks/feishu/status` | GET | Feishu connection summary (app id / target masked, secret never leaves the server) + the scan-session snapshot + the truncation length + a content preview |
|
|
198
202
|
| `/dsh-hooks/feishu/setup` | POST | start a scan session: `{"profile":"web","resultMaxChars":800}`; returns the QR URL / PNG data URL / expiry (409 while one is pending) |
|
|
199
203
|
| `/dsh-hooks/feishu/cancel` | POST | cancel the pending scan session (aborts the registerApp wait) |
|
|
200
204
|
| `/dsh-hooks/feishu/config` | POST | update the card truncation length: `{"resultMaxChars":800}` (50–5000); effective immediately, credentials preserved |
|
|
201
205
|
| `/dsh-hooks/feishu/test` | POST | send a test card with the stored credentials |
|
|
206
|
+
| `/dsh-hooks/feishu/disconnect` | POST | disconnect: delete the credential file; `removeHooks: true` also drops the hooks referencing notify-feishu.mjs (with a backup) |
|
|
202
207
|
|
|
203
|
-
|
|
208
|
+
POSTs require `application/json` in every access mode (blocks cross-site form CSRF). The web profile also gets a systemPrompt section announcing the plugin to agents.
|
|
209
|
+
|
|
210
|
+
### Configure HTTP source IP access
|
|
211
|
+
|
|
212
|
+
Set `DSH_HOOKS_ALLOWED_IPS` in the **environment of the process running `dsh web`**. This is not a `cordis.patch.yml` field and does not require changing your hooks configuration.
|
|
213
|
+
|
|
214
|
+
| Environment variable value | Behavior |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| Unset, empty, or whitespace-only | Allows only `127.0.0.1`, `::1`, and `::ffff:127.0.0.1`, preserving the default behavior |
|
|
217
|
+
| `*` | Disables source IP filtering |
|
|
218
|
+
| `192.168.1.100,10.0.0.2` | Allows only IPs in the comma-separated list |
|
|
219
|
+
|
|
220
|
+
Leading and trailing whitespace is removed. `local` and `all` are not special values: anything other than a blank value or a standalone `*` is matched as an IP list. Allowlist mode **does not implicitly allow loopback connections**; include `127.0.0.1,::1` explicitly if you need local access.
|
|
221
|
+
|
|
222
|
+
Matching ignores surrounding whitespace, letter case, and the `::ffff:` prefix on each address, so `192.168.1.100` matches `::ffff:192.168.1.100`. Hostnames, ports, CIDR ranges, and wildcards within a list are not supported. Invalid entries do not trigger a fallback to loopback-only or unrestricted access. IPv6 matching compares strings after this normalization, without expanding or compressing IPv6 notation; use the representation observed by the server.
|
|
223
|
+
|
|
224
|
+
#### Direct startup
|
|
225
|
+
|
|
226
|
+
PowerShell: choose one setting and start the service in the **same terminal**.
|
|
227
|
+
|
|
228
|
+
```powershell
|
|
229
|
+
# Loopback only (leaving the variable unset also works)
|
|
230
|
+
$env:DSH_HOOKS_ALLOWED_IPS = ''
|
|
231
|
+
|
|
232
|
+
# Alternatively: allow a client and retain local access
|
|
233
|
+
# $env:DSH_HOOKS_ALLOWED_IPS = '192.168.1.100,127.0.0.1,::1'
|
|
234
|
+
|
|
235
|
+
# Alternatively: allow any source IP (secure external access first)
|
|
236
|
+
# $env:DSH_HOOKS_ALLOWED_IPS = '*'
|
|
237
|
+
|
|
238
|
+
dsh web
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Linux/macOS shell: choose one of these commands.
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
DSH_HOOKS_ALLOWED_IPS='' dsh web
|
|
245
|
+
DSH_HOOKS_ALLOWED_IPS='192.168.1.100,127.0.0.1,::1' dsh web
|
|
246
|
+
DSH_HOOKS_ALLOWED_IPS='*' dsh web
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Restart the corresponding `dsh web` process after changing its terminal or service-manager environment; a running process does not inherit subsequent changes. Writing the variable to `.env` alone does not pass it to the process; your launcher or container configuration must load it explicitly.
|
|
250
|
+
|
|
251
|
+
#### Docker Compose
|
|
252
|
+
|
|
253
|
+
Add the variable to `environment` on the **service running DSH**, preserving its existing image, ports, volumes, and other settings. Replace the example service name `dsh` with your actual service name:
|
|
254
|
+
|
|
255
|
+
```yaml
|
|
256
|
+
services:
|
|
257
|
+
dsh:
|
|
258
|
+
environment:
|
|
259
|
+
DSH_HOOKS_ALLOWED_IPS: "192.168.1.100,127.0.0.1,::1"
|
|
260
|
+
# Use "" for loopback only or "*" for unrestricted IPs (quote the asterisk).
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Recreate the service container to apply the new environment, for example with `docker compose up -d --force-recreate dsh`. Restarting an existing container alone does not update its environment configuration. If using a Compose `.env` file, also reference the variable through the service's `environment` or pass it through `env_file`.
|
|
264
|
+
|
|
265
|
+
#### Proxies, security, and verification
|
|
266
|
+
|
|
267
|
+
- The check uses `req.socket.remoteAddress`, ignoring `X-Forwarded-For`, `X-Real-IP`, and `Forwarded`. Behind Docker/NAT/reverse proxies, this may be a gateway or proxy IP instead of the browser machine's IP.
|
|
268
|
+
- Allowlisting a proxy IP allows all clients forwarded by that proxy. A local proxy can also forward external requests as loopback connections. Enforce client restrictions at the proxy. Loopback-only refers to the server's (or container's) loopback connections, not exclusively to browsers on that machine.
|
|
269
|
+
- `*` removes the source IP restriction from sensitive operations including history access, configuration changes, and hook execution. An IP allowlist is not authentication: protect these endpoints with a trusted network or external authentication, and do not expose them directly to untrusted networks.
|
|
270
|
+
- This variable affects only `/dsh-hooks/*`; it does not change listen addresses, ports, firewall rules, or other plugins' permissions.
|
|
271
|
+
|
|
272
|
+
Request `GET /dsh-hooks/status` from both allowed and denied clients using your actual host and port. Requests passing this plugin's check receive the normal status JSON; requests rejected by this plugin receive HTTP 403:
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{"ok":false,"error":{"code":"forbidden","message":"IP not allowed"}}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
If an allowlisted client still receives this error, confirm the variable reached the actual service process, then check whether the server sees the client IP or a proxy/gateway IP. For connection timeouts or refused connections, also check listen addresses, port mappings, and network rules.
|
|
204
279
|
|
|
205
280
|
## Feishu notification example
|
|
206
281
|
|
package/README.zh.md
CHANGED
|
@@ -160,29 +160,104 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # 端到端真跑匹配
|
|
|
160
160
|
|
|
161
161
|
安装后,dsh web 的设置面板里会出现「Hooks」分区(与「通用」「插件」平级):
|
|
162
162
|
|
|
163
|
-
- **状态徽章**:插件版本、hook
|
|
164
|
-
- **手动测试**:选事件(14 类)+ reason/tool,「模拟」看逐 hook
|
|
165
|
-
-
|
|
166
|
-
-
|
|
163
|
+
- **状态徽章**:插件版本、hook 数、历史条数,以及运行诊断(正在执行的 hook 数、最近失败数)
|
|
164
|
+
- **手动测试**:选事件(14 类)+ reason/tool,「模拟」看逐 hook 匹配报告,「执行」真实触发;切换输入自动清空旧结果
|
|
165
|
+
- **通知渠道测试**:向 webhook(可选 Slack 摘要)/ desktop 渠道发一条测试通知,显示发送内容预览
|
|
166
|
+
- **飞书通知**:网页内扫码连接飞书——显示二维码(含有效期倒计时、可取消),扫码后自动创建应用、写入凭据与 hook 配置;已连接后显示应用摘要,可一键发送测试卡片、调整卡片截断长度(50–5000 字符,默认 300,带正文预览)、重新扫码换绑或断开连接(可选一并移除飞书 hooks)
|
|
167
|
+
- **当前 hooks**:只读清单(事件/when/match/run/notify + 超时重试参数),一键「复制 YAML」;点「编辑」进入表单编辑器,增删改 hook 后写回 `cordis.patch.yml`(自动备份原文件、写前校验正则与 run/notify 二选一,保存即热加载)
|
|
168
|
+
- **执行历史时间线**:位于分区底部、**默认折叠**(展开状态记忆于 localStorage;标题旁「展开」查看最近 30 条触发:时间 / 事件 / 命令 / 结果 / stderr 尾部),5 秒自动刷新
|
|
167
169
|
|
|
168
170
|
CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心零 UI 运行时依赖。
|
|
169
171
|
|
|
170
172
|
## Web profile HTTP 路由
|
|
171
173
|
|
|
172
|
-
web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册
|
|
174
|
+
web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 `/dsh-hooks/*` 路由,默认仅允许本地回环地址,可通过下述环境变量调整——CLI/headless 环境完全无感:
|
|
173
175
|
|
|
174
176
|
| 路由 | 方法 | 用途 |
|
|
175
177
|
| --- | --- | --- |
|
|
176
|
-
| `/dsh-hooks/status` | GET | 插件版本、hook
|
|
178
|
+
| `/dsh-hooks/status` | GET | 插件版本、hook 数、历史条数、**当前 hooks 清单**与运行统计 |
|
|
177
179
|
| `/dsh-hooks/history?n=50` | GET | 最近 N 条执行历史(JSON envelope) |
|
|
178
180
|
| `/dsh-hooks/test` | POST | 模拟事件评估:`{"event":"tool/call","tool":"ssh_exec","execute":false}` 返回逐 hook 匹配报告;`execute: true` 真跑匹配的 hook |
|
|
179
|
-
| `/dsh-hooks/
|
|
181
|
+
| `/dsh-hooks/notify/test` | POST | 向指定渠道发测试通知:`{"channel":"webhook","url":…,"slack":true}` 或 `{"channel":"desktop"}`,返回发送内容预览 |
|
|
182
|
+
| `/dsh-hooks/hooks/save` | POST | 保存 hook 列表:`{"profile":"web","hooks":[…]}`——校验(事件/when/正则/run-notify 二选一)后写回 cordis.patch.yml,自动备份原文件 |
|
|
183
|
+
| `/dsh-hooks/feishu/status` | GET | 飞书连接摘要(app id / 目标均已打码,绝不返回 secret)+ 扫码会话快照 + 截断长度 + 正文预览 |
|
|
180
184
|
| `/dsh-hooks/feishu/setup` | POST | 启动扫码会话:`{"profile":"web","resultMaxChars":800}`,返回二维码 URL / PNG data URL / 有效期;进行中时再次请求返回 409 |
|
|
181
185
|
| `/dsh-hooks/feishu/cancel` | POST | 取消进行中的扫码会话(中止 registerApp 等待) |
|
|
182
186
|
| `/dsh-hooks/feishu/config` | POST | 更新卡片截断长度:`{"resultMaxChars":800}`(50–5000),即时生效,保留凭据 |
|
|
183
187
|
| `/dsh-hooks/feishu/test` | POST | 用已存凭据发送测试卡片 |
|
|
188
|
+
| `/dsh-hooks/feishu/disconnect` | POST | 断开连接:删除凭据文件,`removeHooks: true` 时一并移除 patch 中引用 notify-feishu.mjs 的 hooks(带备份) |
|
|
184
189
|
|
|
185
|
-
|
|
190
|
+
所有访问模式下,POST 仍必须使用 `application/json`(防跨站表单 CSRF)。同时 web profile 下会向 agent 注入一段 systemPrompt 公告,说明插件存在与协作方式。
|
|
191
|
+
|
|
192
|
+
### 配置 HTTP 来源 IP 限制
|
|
193
|
+
|
|
194
|
+
在**运行 `dsh web` 的进程环境**中设置 `DSH_HOOKS_ALLOWED_IPS`。这不是 `cordis.patch.yml` 的配置字段,不需要修改 hooks 配置。
|
|
195
|
+
|
|
196
|
+
| 环境变量值 | 行为 |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| 未设置、空字符串或只有空白 | 仅允许 `127.0.0.1`、`::1`、`::ffff:127.0.0.1`,保持默认行为 |
|
|
199
|
+
| `*` | 不限制来源 IP |
|
|
200
|
+
| `192.168.1.100,10.0.0.2` | 只允许逗号分隔列表中的 IP |
|
|
201
|
+
|
|
202
|
+
变量值首尾空白会被去除。`local`、`all` 不是特殊值;除空值和单独的 `*` 外,其他值都作为 IP 列表匹配。白名单模式**不会额外放行本地连接**,如需保留本地访问,请显式加入 `127.0.0.1,::1`。
|
|
203
|
+
|
|
204
|
+
匹配时会忽略每项首尾空白、字母大小写及 `::ffff:` 前缀,例如 `192.168.1.100` 可以匹配 `::ffff:192.168.1.100`。不支持域名、端口、CIDR 网段或列表内通配符;无效条目不会自动回退到仅本地或不限制模式。IPv6 采用上述规则处理后的字符串比较,不会统一展开/压缩写法,请使用与服务端所见地址一致的写法。
|
|
205
|
+
|
|
206
|
+
#### 直接启动
|
|
207
|
+
|
|
208
|
+
PowerShell:选择一种设置,在**同一终端**启动服务。
|
|
209
|
+
|
|
210
|
+
```powershell
|
|
211
|
+
# 仅本地(不设置该变量也可以)
|
|
212
|
+
$env:DSH_HOOKS_ALLOWED_IPS = ''
|
|
213
|
+
|
|
214
|
+
# 或:允许指定客户端,并保留本地访问
|
|
215
|
+
# $env:DSH_HOOKS_ALLOWED_IPS = '192.168.1.100,127.0.0.1,::1'
|
|
216
|
+
|
|
217
|
+
# 或:不限制来源 IP(请先确保外部访问控制可靠)
|
|
218
|
+
# $env:DSH_HOOKS_ALLOWED_IPS = '*'
|
|
219
|
+
|
|
220
|
+
dsh web
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Linux/macOS shell:以下命令三选一。
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
DSH_HOOKS_ALLOWED_IPS='' dsh web
|
|
227
|
+
DSH_HOOKS_ALLOWED_IPS='192.168.1.100,127.0.0.1,::1' dsh web
|
|
228
|
+
DSH_HOOKS_ALLOWED_IPS='*' dsh web
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
修改终端或服务管理器中的环境变量后,需要重新启动对应的 `dsh web` 进程;已运行的进程不会自动继承新值。单独把变量写入 `.env` 不代表已经传入进程,需要由启动器或容器配置明确加载。
|
|
232
|
+
|
|
233
|
+
#### Docker Compose
|
|
234
|
+
|
|
235
|
+
将变量加入**实际运行 DSH 的服务**的 `environment`,保留原有镜像、端口、卷等配置。以下 `dsh` 为示例服务名,请替换成自己的服务名:
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
services:
|
|
239
|
+
dsh:
|
|
240
|
+
environment:
|
|
241
|
+
DSH_HOOKS_ALLOWED_IPS: "192.168.1.100,127.0.0.1,::1"
|
|
242
|
+
# 仅本地用 "";不限制用 "*"(星号必须加引号)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
修改后重新创建该服务的容器以应用新环境变量,例如 `docker compose up -d --force-recreate dsh`;仅重启已有容器不会更新容器环境配置。若使用 Compose 的 `.env` 文件,也需要在服务中通过 `environment` 引用变量或通过 `env_file` 传入。
|
|
246
|
+
|
|
247
|
+
#### 代理、安全与验证
|
|
248
|
+
|
|
249
|
+
- 检查的是 `req.socket.remoteAddress`,不读取 `X-Forwarded-For`、`X-Real-IP` 或 `Forwarded`。Docker/NAT/反向代理下,该地址可能是网关或代理 IP,而不是浏览器所在机器的 IP。
|
|
250
|
+
- 放行代理 IP 会放行经该代理转发的所有客户端;本机代理也可能将外部请求转发为回环连接。因此,代理后的客户端限制应在代理层执行。“仅本地”指服务端(或容器内)的回环连接,不等于只允许本机浏览器。
|
|
251
|
+
- `*` 会放开读取历史、修改配置、执行 hook 等敏感接口的来源 IP 限制;IP 白名单不是身份认证,请用可信网络或外部认证保护这些接口,不要直接暴露到不可信网络。
|
|
252
|
+
- 此变量只影响 `/dsh-hooks/*`,不会改变服务监听地址、端口、防火墙规则或其他插件的权限。
|
|
253
|
+
|
|
254
|
+
可从允许及不允许的客户端分别请求 `GET /dsh-hooks/status`(主机和端口替换为实际地址)。通过本插件的检查时返回正常状态 JSON;被本插件拒绝时返回 HTTP 403:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{"ok":false,"error":{"code":"forbidden","message":"IP not allowed"}}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
若白名单配置后仍收到该错误,先确认变量已传入实际服务进程,再检查服务端看到的是客户端 IP 还是代理/网关 IP。若连接超时或被拒绝连接,则还需检查监听地址、端口映射和网络规则。
|
|
186
261
|
|
|
187
262
|
## 通用 webhook 示例
|
|
188
263
|
|