🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI
广告☁️ 云服务器特惠阿里云首购 8 折 · 腾讯云合作特惠
DeepSeek Harness Hub
← 返回列表

PeterBon/dsh-hooks

DeepSeek Harnessspec-screened扫描:中风险在 GitHub 查看 ↗
✓ 可直接安装

DeepSeek Harnessdsh的配置驱动生命周期 hooks 插件。

自动检查通过:npm 包已发布且 engines 声明满足基线(声明 Node >=22);该结论来自程序自动检查,未经人工实机验证。 · 最近上游提交 2026/9/22 · 已提供中文文档

DeepSeek Harness 的配置驱动生命周期钩子插件

综合分
38.2
GitHub 分
38.2
用户评分
—
★ Stars
6
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add dsh-hooks
npm 包 dsh-hooks 已校验归属本仓库,走 npm 安装最省事
信任档位:已验证本站已于 4 天前真实安装成功(L4 · 真实安装)
是什么
dsh 原生插件 · platform
装得上吗
本站已真实安装成功(L4 · 真实安装,非静态推断)
安全吗
本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
还在维护吗
活跃:最近一次提交在 3 天前

档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →

🟢实装验证通过· 2026/9/21
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/23(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查✓ 自动检查通过

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-hooks @ 0.13.1
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/21 20:15:12

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-runtime@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-session@deepseek-ai/schemastery
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

由 DeepSeek 最新模型翻译生成
dsh-hooks

DeepSeek Harness(dsh)的配置驱动生命周期 hooks 插件。

直接在 profile 的 cordis.patch.yml 里声明「事件 → 命令」——就像 Codex CLI / OpenCode 的 hooks,但属于 dsh。不需要写插件代码。

安装

一个包搞定全部(hook 引擎 + Web GUI 设置页):

dsh plugin --profile web add dsh-hooks           # 从 npm 安装
或直接从 git 安装:
dsh plugin --profile web add github:PeterBon/dsh-hooks

重启 dsh web 生效。安装后设置面板里会出现「Hooks」分区(见 Web GUI)。

配置

在你的 profile 的 cordis.patch.yml 里添加配置块:

- id: dsh-hooks
name: dsh-hooks
config:
hooks:
- on: 'turn/end'
when: 'completed'            # 可选:只在回合正常完成时触发
run: 'node examples/notify-feishu.mjs'
timeoutMs: 10000             # 可选,默认 10000
- on: 'approval/asked'
run: 'powershell -Command "Add-Content hooks.log approval-requested"'
- on: 'tool/call'
match:                       # 可选:字段 → 正则,全部匹配才触发
tool: '^(rm|git|ssh)'
run: 'node examples/notify-webhook.mjs --slack'
- on: 'turn/end'
when: 'completed'
run: 'node examples/notify-feishu.mjs'
retries: 2                   # 可选:非零退出码重试(默认 0 不重试)
retryDelayMs: 1000           # 可选:重试基础间隔,每次翻倍(默认 500)
- on: 'turn/end'
input: 'stdin'               # 可选:把完整上下文 JSON 写入命令 stdin
run: 'node my-hook.mjs'
- on: 'approval/asked'
notify:                      # 内置通知:与 run 二选一,无需外部脚本
channel: 'desktop'         # 桌面气泡/toast 通知
- on: 'turn/end'
when: 'completed'
notify:
channel: 'webhook'         # POST JSON 到任意 HTTP 端点
url: 'https://hooks.slack.com/services/…'
slack: true                # 可选:改为 { text } 单行摘要(Slack 风格)
- on: 'step/end'
run: 'node examples/log-step.mjs'
debounceMs: 500              # 可选:高频事件去抖,窗口内合并为一次
maxConcurrent: 2             # 可选:并发上限,超出的触发被丢弃
- on: 'tool/result'
match:
toolDurationMs: '>10000'   # 数值比较(也支持 { gt: 10000 } 对象语法)
run: 'node examples/notify-slow-tool.mjs'
- on: 'turn/end'
enabled: false               # 可选:停用但不删除(跳过派发)
cwd: 'session'               # 可选:在会话工作目录执行
run: 'node examples/log-turn.mjs'

每个 hook 的完整字段:

| 字段 | 含义 | 默认 |
| --- | --- | --- |
| on | 触发事件(见上方事件表) | 必填 |
| when | 对 turn/end 按结束原因过滤 | 全部原因 |
| match | 字段 → 正则或数值比较,全部匹配才触发;字段为上下文键(tool/sessionName/sessionId/error/source/cwd/content/reason/turn/durationMs/toolDurationMs…),上下文中不存在的字段视为不匹配。正则匹配字段的字符串表示;数值比较({ gt: 10000 } 或 '>10000',支持 gt/gte/lt/lte/eq 组合)只对数字字段生效,非数字字段上的比较永不匹配 | 不过滤 |
| run | 通过系统 shell 执行的命令(与 notify 二选一) | 二选一必填 |
| notify | 内置通知(与 run 二选一):channel: webhook(HTTP JSON,url 可省略用 DSH_HOOKS_WEBHOOK_URL,slack: true 换单行摘要)或 channel: desktop(系统气泡/toast) | 二选一必填 |
| input | env 只传 DSH_HOOK_ 环境变量;stdin 额外把完整上下文 JSON 写入命令标准输入 | env |
| timeoutMs | 单次执行超时(毫秒),超时终止进程树 | 10000 |
| retries | 非零退出码的重试次数(spawn 失败与超时不重试) | 0 |
| retryDelayMs | 重试基础间隔(毫秒),每次翻倍 | 500 |
| enabled | false 停用该 hook:配置保留、静默跳过派发(不计入失败) | true |
| cwd | 命令执行目录:session 在会话工作目录执行;绝对路径在指定目录执行(只作用于 run) | 插件进程目录 |
| maxConcurrent | 该 hook 允许的最大并发进程数;超出的触发被丢弃(执行历史记 skipped) | 不限 |
| debounceMs | 去抖窗口(毫秒):高频事件(step/end、tool/…)窗口内的多次触发合并为一次 trailing 执行,携带最新上下文 | 0(不去抖) |

事件(v1)

| 事件 | 触发时机 | 有用上下文 |
| --- | --- | --- |
| turn/start | 回合开始(若有 turn/start hook,派发延迟到本回合的首条用户消息分类后,把触发文本注入 DSH_HOOK_CONTENT;无用户消息的回合在 turn/end 时无内容派发,见下方说明) | 会话 id、回合号、触发消息文本 |
| turn/end | 回合结束(completed / error / aborted / blocked / max-tokens / interrupted) | reason、回合号、耗时、内容、本回合 token 用量、运行中子代理数 |
| tree/settled | 回合结束后把工作交给子代理的会话,其整个子代理树全部落定(无存活子代理仍在运行) | 子代理总数、交接到落定的耗时 |
| step/end | 回合内一步结束(一次模型调用 + 其工具执行) | 回合号、步号 |
| tool/call | 模型请求一次工具调用 | 工具名、调用 id、原始参数 JSON |
| tool/result | 工具调用完成 | 工具名(自动反查)、结果文本、失败标识、工具耗时(配对失败时无耗时) |
| user/message | 会话表面出现用户角色消息 | 来源 kind(user / plugin / …)、消息文本 |
| approval/asked | 工具调用请求用户审批 | 工具名、调用 id、审批 id、原因 |
| approval/decided | 待审批项得出结果(与 approval/asked 按 id 配对) | 结果 outcome、工具名(自动反查)、调用 id、审批 id |
| session/title | 会话标题更新(显式改名 / LLM 生成 / 回退) | 新标题、来源 kind |
| session/created | 会话发布 | 会话 id、cwd |
| session/disposed | 会话离开注册表 | 会话 id、cwd |
| agent/created | Agent 发布 | 会话 id |
| agent/disposed | Agent 离开注册表 | 会话 id |
| agent/error | Agent 循环报错 | 错误文本 |
| agent/status | Agent 状态切换 | 状态 |
| hook/failed | 同一 hook 连续失败达到 failedAlertThreshold(默认 3;合成事件,从结果流发射) | 失败 hook 摘要、连续失败次数 |
| usage/daily | 本地日历日翻篇后的下一个事件(合成事件,无定时器):报告刚结束那一天的 token 用量 | 覆盖日期、当日回合数、贡献会话数、当日 token 明细 |

turn/end 的 when 匹配结束原因(completed、error…);其他事件的 hook 无条件执行。

命令执行

- 每个命中的 hook 通过系统 shell 执行 run,fire-and-forget:失败只 console.warn、默认不重试、绝不阻塞 agent 循环。命令的 stdout/stderr 会被捕获(各 64 KiB 上限),非零退出码时把 stderr 尾部写进告警日志。
- 重试(retries / retryDelayMs)对两个执行通道都生效,都是「首次尝试之后再重试 N 次」、间隔按 retryDelayMs 逐次翻倍(默认 500ms):
- run:只重试非零退出码(spawn 失败与超时不重试)。
- notify 的 webhook 渠道:重试传输失败(连接被重置、超时)与 HTTP 408 / 429 / 5xx;其余 4xx 表示请求本身有问题,不重试。默认 retries: 0 表示只尝试一次——0.13 之前 webhook 曾硬编码「传输失败自动再试一次」,现在这层兜底已并入 retries,需要它的老配置请显式写 retries: 1。
- notify 的 desktop 渠道是本地弹窗,不重试。
- 上下文通过环境变量传递(数据不拼接进 shell 字符串,防注入):

| 变量 | 含义 |
| --- | --- |
| DSH_HOOK_EVENT | 事件类型,如 turn/end |
| DSH_HOOK_SESSION_ID | 会话 id |
| DSH_HOOK_SESSION_NAME | 会话可读标题(最新 session/title 日志事件,或首个用户消息回退) |
| DSH_HOOK_CWD | 会话工作目录 |
| DSH_HOOK_TURN | 回合号(回合 / 步骤 / 工具事件) |
| DSH_HOOK_STEP | 步号(步骤 / 工具事件) |
| DSH_HOOK_REASON | 回合结束原因 |
| DSH_HOOK_TOOL | 工具名(审批 / 工具事件) |
| DSH_HOOK_CALL_ID | 工具调用 id(审批 / 工具事件) |
| DSH_HOOK_TOOL_ARGS | 工具原始参数 JSON(tool/call) |
| DSH_HOOK_TOOL_ERROR | 工具失败标识 名称: 代码(tool/result 出错时) |
| DSH_HOOK_TOOL_DURATION_MS | 工具执行耗时毫秒(tool/result;配对 tool/call 丢失时无此变量) |
| DSH_HOOK_SOURCE | 消息 / 标题来源 kind(user、plugin、fallback、provider…) |
| DSH_HOOK_DURATION_MS | 回合耗时毫秒(turn/end) |
| DSH_HOOK_STATUS | Agent 状态(agent/status) |
| DSH_HOOK_ERROR | 错误文本(agent/error,以及 turn/end 出错时的失败详情) |
| DSH_HOOK_CONTENT | 事件内容快照:回合最后助手文本、工具结果文本、用户消息文本、回合触发消息文本(turn/start) |
| DSH_HOOK_USAGE_INPUT_TOKENS | 输入 token 总量(turn/end 为本回合、逐 step 聚合;usage/daily 为当日聚合) |
| DSH_HOOK_USAGE_OUTPUT_TOKENS | 输出 token 总量(同上) |
| DSH_HOOK_USAGE_CACHE_READ_TOKENS | 缓存读 token(有上报时,同上) |
| DSH_HOOK_USAGE_CACHE_WRITE_TOKENS | 缓存写 token(有上报时,同上) |
| DSH_HOOK_USAGE_REASONING_TOKENS | 思考 token(有上报时,同上) |
| DSH_HOOK_RUNNING_SUBAGENTS | 本会话下仍在运行的存活子代理数(turn/end;0 = 无——让 hook 能区分「工作已交给后台子代理」与「回合真正结束」) |
| DSH_HOOK_PARENT_SESSION_ID | 父会话 id(子代理谱系;顶层会话无此变量) |
| DSH_HOOK_SUBAGENT | 会话为子代理时为 1,否则 0 |
| DSH_HOOK_DELEGATION_DEPTH | 会话头中的委托深度(0 = 顶层会话) |
| DSH_HOOK_SESSION_CREATED_AT | 会话创建时间,epoch 毫秒 |
| DSH_HOOK_AGENT_PRESET | 组合该会话 Agent 的预设 id(有值时) |
| DSH_HOOK_APPROVAL_ID | 审批审计 id(approval/asked 与 approval/decided 共用) |
| DSH_HOOK_APPROVAL_OUTCOME | 审批结果 outcome(approval/decided) |
| DSH_HOOK_TOTAL_SUBAGENTS | 已落定树中的子代理总数(tree/settled) |
| DSH_HOOK_TREE_DURATION_MS | 父回合结束 → 树落定的耗时(毫秒,tree/settled) |
| DSH_HOOK_FAILED_HOOK | 连续失败的 hook 身份摘要(hook/failed) |
| DSH_HOOK_FAILURES | 告警触发时的连续失败次数(hook/failed) |
| DSH_HOOK_USAGE_DAY | 报告覆盖的本地日历日 YYYY-MM-DD(usage/daily) |
| DSH_HOOK_USAGE_TURNS | 当日计入的回合数(usage/daily) |
| DSH_HOOK_USAGE_SESSIONS | 当日贡献用量的会话数(usage/daily) |
| DSH_HOOK_TIMESTAMP | ISO 时间戳 |

- run 里的 {{变量}} 占位符会从同一上下文替换,例如 run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'。
- 失败告警:fire-and-forget 的 hook 失败本来就是静默的,插件因此同时监视结果流——同一 hook 连续失败 failedAlertThreshold 次(spawn-failed / exit-nonzero / timeout / send-failed;一次逻辑执行的最终结果计一次,内部重试不另计)后发射合成事件 hook/failed,每条失败链只发一次;成功会清零计数并解除去抖。用普通 hook 接告警即可:

config:
failedAlertThreshold: 3   # 可选,默认 3
hooks:
- on: 'hook/failed'
notify: { channel: 'desktop' }
- on: 'turn/end'
run: 'node my-hook.mjs'

- turn/end 的 hook 在运行中子代理计数解析完成后才派发,比其他事件晚一个异步跳——同会话紧随其后的事件(如下一轮 turn/start)可能先执行。

DSH_HOOK_RUNNING_SUBAGENTS 的典型用法:后台子代理还在运行时抑制回合结束通知,只在本会话回合真正落定时才通知。注意父会话只会收到一次 turn/end(此时计数 > 0);「全部落定」的信号由最后一个子会话自己的 turn/end(计数为 0)送达:

- on: 'turn/end'
match: { runningSubagents: '^0$' }  # 正则要锚定:裸 '0' 也会匹配 '10'
run: 'node examples/notify-webhook.mjs'

如果只想要「整棵树落定才通知一次」的简单模式,合成事件 tree/settled 帮你做了监视:插件跟踪回合结束时仍有运行中子代理的会话,树归零时对该会话发射 tree/settled:

- on: 'tree/settled'
notify: { channel: 'webhook', url: 'https://hooks.slack.com/services/…' }

已落定但闲置(idle)的 continuable 子代理不计入运行中,不会一直压住通知。落定监视是事件驱动且 best-effort 的:插件重启后监视集合丢失;重查失败会静默放弃该监视(不会补发迟到的通知)。

usage/daily:跨日 token 日报

turn/end 只回答「这个回合花了多少」。要按天看成本,用合成事件 usage/daily:插件在内存里按本地日历日累计每个 turn/end 上报的 token(子代理会话的回合一并计入——同一个账号),日期翻篇后对下一个到达的事件发射一次日报,报告刚结束的那一天。检测纯事件驱动、无定时器、无定时任务。

- on: 'usage/daily'
match: { usageInputTokens: '>0' }     # 可选:跳过没有用量的日子
run: 'node examples/log-usage.mjs'    # 或 notify: { channel: 'webhook', url: '…' }

DSH_HOOK_USAGE_DAY 是报告覆盖的日期(YYYY-MM-DD);DSH_HOOK_USAGE_TURNS / DSH_HOOK_USAGE_SESSIONS 是当日计入的回合数与贡献会话数;token 明细沿用 turn/end 的 DSH_HOOK_USAGE_ 变量名(usageInputTokens / usageOutputTokens / usageCacheReadTokens / usageCacheWriteTokens / usageReasoningTokens),语义变为「该日聚合」。

三条边界(按设计,不是 bug):

- 内存累计:插件进程重启会丢掉进行中那一天的累计(重启后从新的一天、从零开始);已发出的日报不受影响。
- 事件驱动而非定时:一天的用量要等下一个事件到达才报告,所以跨夜后若一直没动静,日报会推迟到下一次有事件时补发;那一天从未有回合上报用量则不发射(空日报是噪声)。
- 零开销:没有声明任何 usage/daily hook 时,插件完全不做累计与跨日检测。

dsh-hooks dry-run usage/daily 用「昨天」和非零 token 模拟一次日报,可先验证 match 与命令。

match 数值比较

对数字字段(turn、step、durationMs、toolDurationMs、usage、runningSubagents…)可以直接写数值比较,不用绕正则:

- on: 'tool/result'
match: { toolDurationMs: '>10000' }   # 字符串语法:> >= />=/10000');其余字符串仍是普通正则。
- 字段缺失照旧视为不匹配。空对象 {} 恒真(无任何条件)。

执行选项:enabled / cwd / maxConcurrent / debounceMs

每个 hook 都可以独立微调执行方式:

- enabled: false:停用该 hook 但保留配置。跳过是静默的——不记执行历史、不计入失败链(hook/failed 不会因停用的 hook 触发)。dry-run 会标出 enabled: false(已停用)。
- cwd: 'session':在会话工作目录(agent 正在工作的项目目录)执行 run,方便 hook 脚本直接读写当前项目文件;cwd 也接受绝对路径。缺省在插件进程目录执行。
- maxConcurrent:并发进程上限。超过上限的触发被丢弃并记入执行历史(skipped,不触发失败告警);一次逻辑执行(含其内部重试)始终占用一个名额。
- debounceMs:去抖窗口。高频事件(step/end、tool/…)窗口内的多次触发合并为一次 trailing 执行,携带最新一次的上下文;被合并掉的触发完全静默,不会刷日志/历史。窗口结束后的新触发正常执行。

防 step/end/tool/ spawn 风暴的推荐组合:
yaml
- on: 'step/end'
run: 'node examples/log-step.mjs'
debounceMs: 500       # 半秒内的连续步结束只跑一次
maxConcurrent: 2      # 兜底:命令变慢时并发不超过 2

turn/start 的触发内容

会话日志先记录 turn/start、后记录该回合的 user/message,所以回合开始那一刻还读不到触发文本。插件因此把 turn/start 的派发延迟到本回合首条直接用户消息分类后,把消息文本注入 DSH_HOOK_CONTENT(截断 2000 字符):
yaml
- on: 'turn/start'
match: { content: '部署|发版' }   # 只关心包含关键词的回合
notify: { channel: 'desktop' }

时序说明:

- 有 turn/start hook 时才启用延迟;没有的话派发保持原样(立即执行,无内容)。
- 注入文本只认 source.kind === 'user' 的直接用户消息;系统注入(agent/plugin 来源)不会完成派发。
- 回合内没有直接用户消息(如目标续跑回合)时,turn/start 在 turn/end 时不带内容派发;新回合开始会先冲掉上一个未认领的 turn/start。
- 直接用户回合中,延迟通常只有几毫秒(user/message 紧随 turn/start),先于任何步骤/工具事件。

执行历史

每次 hook 触发都会记入内存环形缓冲(默认 500 条),并 best-effort 追加到 ~/.dsh/dsh-hooks/history.jsonl(权限 0600)——供未来 UI 与调试使用。环形缓冲在启动时从 JSONL 回填,且 Web 面板每次读取时增量同步磁盘上新增的记录(包括其他 dsh 进程的追加,如任务看板 Host),因此重启后历史不会消失。记录不含 secret(环境变量从不入记录):
yaml
- id: dsh-hooks
name: dsh-hooks
config:
history:
enabled: true        # 可选:持久化到磁盘(默认 true)
max: 500             # 可选:内存环形缓冲条数
path: '…'          # 可选:自定义 JSONL 路径(默认 ~/.dsh/dsh-hooks/history.jsonl)
hooks: […]

每条记录:时间戳、kind(run/notify)、事件、命令、会话、结果(spawned / exit-0 / exit-nonzero / timeout / skipped / sent / send-failed…)、退出码、耗时、stderr 尾部。写盘失败静默吞掉,绝不阻塞 hook。

终端里想边跑边看,用 tail(Ctrl+C 退出):
sh
dsh-hooks tail                                  # 回放最近 10 条,然后实时跟进
dsh-hooks tail --event turn/end --outcome exit-nonzero   # 只看失败的回合结束
dsh-hooks tail --hook notify-feishu --n 50 --json         # 50 条起,输出原始 JSONL 供 jq

tail 的 JSONL 路径取自 profile 配置的 history.path(未配置或配置读不出来时回落到默认路径,不会因为配置文件半途改动而报错)。它只读新增字节、容忍半行(等换行再输出)、文件被截断/轮转时自动从 0 重新跟进。

dry-run:验证配置

配置完先用 dry-run 模拟事件,看哪些 hook 会触发、哪些被过滤:
sh
dsh-hooks dry-run turn/end --reason completed --profile web
✅ [1] [turn/end when=completed] run: node notify-feishu.mjs
⏭ [2] [turn/end when=error] run: … —— when 不匹配(期望 error,实际 completed)
⏭ [3] [tool/call] run: … —— 事件不匹配(tool/call ≠ turn/end)
共 1 个 hook 会触发。加 --execute 实际执行(真实副作用!)

dsh-hooks dry-run tool/call --tool ssh_exec --execute   # 端到端真跑匹配的 hook

模拟数值字段:数字类上下文(runningSubagents、durationMs、toolDurationMs、usage…)可以直接给定值,用来验证基于数值的 match:
sh
dsh-hooks dry-run turn/end --running-subagents 3
dsh-hooks dry-run turn/end --duration-ms 1250 --usage-input 120000 --usage-output 45000
dsh-hooks dry-run tool/result --tool-duration-ms 15000
dsh-hooks dry-run usage/daily --field usageCacheReadTokens=90000   # 通用写法:--field =

可模拟字段白名单:turn、step、durationMs、toolDurationMs、runningSubagents、totalSubagents、treeDurationMs、usageTurns、usageSessions、usageInputTokens、usageOutputTokens、usageCacheReadTokens、usageCacheWriteTokens、usageReasoningTokens。非白名单字段或非法数字不会被静默丢弃——报告里会列出「已忽略无法模拟的字段」。

模拟上下文与运行时保持一致:turn/end 一定带 runningSubagents(默认 0),所以 README 推荐的 match: { runningSubagents: '^0$' } 在 dry-run 里也能命中;usage/daily 带「昨天」与非零 token 明细。

dry-run 直接读 profile 的 cordis.patch.yml(id: dsh-hooks 配置块),配置校验(非法正则等)会在这一步报错。

Web GUI

安装后,dsh web 的设置面板里会出现「Hooks」分区(与「通用」「插件」平级):

- 状态徽章:插件版本、hook 数、历史条数,以及运行诊断(正在执行的 hook 数、最近失败数)
- 手动测试:选事件(18 类)+ reason/tool,并可用「模拟字段」一行填入 runningSubagents / durationMs / usage 输入输出等数值上下文;「模拟」看逐 hook 匹配报告,「执行」真实触发;切换输入自动清空旧结果
- 通知渠道测试:向 webhook(可选 Slack 摘要)/ desktop 渠道发一条测试通知,显示发送内容预览
- 飞书通知:网页内扫码连接飞书——显示二维码(含有效期倒计时、可取消),扫码后自动创建应用、写入凭据与 hook 配置;已连接后显示应用摘要,可一键发送测试卡片、调整卡片截断长度(50–5000 字符,默认 300,带正文预览)、重新扫码换绑或断开连接(可选一并移除飞书 hooks)
- 当前 hooks:只读清单(事件/when/match/run/notify + 超时重试参数),一键「复制 YAML」;点「编辑」进入表单编辑器,增删改 hook 后写回 cordis.patch.yml(自动备份原文件、写前校验正则与 run/notify 二选一,保存即热加载)
- 执行历史时间线:位于分区底部、默认折叠(展开状态记忆于 localStorage),5 秒自动刷新。展开后可按事件 / 结果 / 会话过滤(条件同样记忆在 localStorage,附「显示 N / 共 M 条」计数与「清空过滤」),并把当前视图导出为 JSONL(与磁盘上的 history.jsonl 同格式,文件名带本地时间戳)——面板一次拉取最近 200 条,过滤在浏览器侧完成

CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心零 UI 运行时依赖。

Web profile HTTP 路由

web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 /dsh-hooks/ 路由,默认仅允许本地回环地址,可通过下述环境变量调整——CLI/headless 环境完全无感:

| 路由 | 方法 | 用途 |
| --- | --- | --- |
| /dsh-hooks/status | GET | 插件版本、hook 数、历史条数、当前 hooks 清单与运行统计 |
| /dsh-hooks/history?n=50 | GET | 最近 N 条执行历史(JSON envelope) |
| /dsh-hooks/test | POST | 模拟事件评估:{"event":"tool/call","tool":"ssh_exec","execute":false} 返回逐 hook 匹配报告;可用 fields 覆盖数值上下文({"event":"turn/end","fields":{"runningSubagents":2}},非法字段返回 400);execute: true 真跑匹配的 hook |
| /dsh-hooks/notify/test | POST | 向指定渠道发测试通知:{"channel":"webhook","url":…,"slack":true} 或 {"channel":"desktop"},返回发送内容预览 |
| /dsh-hooks/hooks/save | POST | 保存 hook 列表:{"profile":"web","hooks":[…]}——校验(事件/when/正则/run-notify 二选一)后写回 cordis.patch.yml,自动备份原文件 |
| /dsh-hooks/feishu/status | GET | 飞书连接摘要(app id / 目标均已打码,绝不返回 secret)+ 扫码会话快照 + 截断长度 + 正文预览 |
| /dsh-hooks/feishu/setup | POST | 启动扫码会话:{"profile":"web","resultMaxChars":800},返回二维码 URL / PNG data URL / 有效期;进行中时再次请求返回 409 |
| /dsh-hooks/feishu/cancel | POST | 取消进行中的扫码会话(中止 registerApp 等待) |
| /dsh-hooks/feishu/config | POST | 更新卡片截断长度:{"resultMaxChars":800}(50–5000),即时生效,保留凭据 |
| /dsh-hooks/feishu/test | POST | 用已存凭据发送测试卡片 |
| /dsh-hooks/feishu/disconnect | POST | 断开连接:删除凭据文件,removeHooks: true 时一并移除 patch 中引用 notify-feishu.mjs 的 hooks(带备份) |

所有访问模式下,POST 仍必须使用 application/json(防跨站表单 CSRF)。同时 web profile 下会向 agent 注入一段 systemPrompt 公告,说明插件存在与协作方式。

配置 HTTP 来源 IP 限制

在运行 dsh web 的进程环境中设置 DSH_HOOKS_ALLOWED_IPS。这不是 cordis.patch.yml 的配置字段,不需要修改 hooks 配置。

| 环境变量值 | 行为 |
| --- | --- |
| 未设置、空字符串或只有空白 | 仅允许 127.0.0.1、::1、::ffff:127.0.0.1,保持默认行为 |
|  | 不限制来源 IP |
| 192.168.1.100,10.0.0.2 | 只允许逗号分隔列表中的 IP |

变量值首尾空白会被去除。local、all 不是特殊值;除空值和单独的  外,其他值都作为 IP 列表匹配。白名单模式不会额外放行本地连接,如需保留本地访问,请显式加入 127.0.0.1,::1。

匹配时会忽略每项首尾空白、字母大小写及 ::ffff: 前缀,例如 192.168.1.100 可以匹配 ::ffff:192.168.1.100。不支持域名、端口、CIDR 网段或列表内通配符;无效条目不会自动回退到仅本地或不限制模式。IPv6 采用上述规则处理后的字符串比较,不会统一展开/压缩写法,请使用与服务端所见地址一致的写法。

直接启动

PowerShell:选择一种设置,在同一终端启动服务。
powershell
仅本地(不设置该变量也可以)
$env:DSH_HOOKS_ALLOWED_IPS = ''

或:允许指定客户端,并保留本地访问
$env:DSH_HOOKS_ALLOWED_IPS = '192.168.1.100,127.0.0.1,::1'

或:不限制来源 IP(请先确保外部访问控制可靠)
$env:DSH_HOOKS_ALLOWED_IPS = ''

dsh web

Linux/macOS shell:以下命令三选一。
sh
DSH_HOOKS_ALLOWED_IPS='' dsh web
DSH_HOOKS_ALLOWED_IPS='192.168.1.100,127.0.0.1,::1' dsh web
DSH_HOOKS_ALLOWED_IPS='' dsh web

修改终端或服务管理器中的环境变量后,需要重新启动对应的 dsh web 进程;已运行的进程不会自动继承新值。单独把变量写入 .env 不代表已经传入进程,需要由启动器或容器配置明确加载。

Docker Compose

将变量加入实际运行 DSH 的服务的 environment,保留原有镜像、端口、卷等配置。以下 dsh 为示例服务名,请替换成自己的服务名:
yaml
services:
dsh:
environment:
DSH_HOOKS_ALLOWED_IPS: "192.168.1.100,127.0.0.1,::1"
仅本地用 "";不限制用 ""(星号必须加引号)

修改后重新创建该服务的容器以应用新环境变量,例如 docker compose up -d --force-recreate dsh;仅重启已有容器不会更新容器环境配置。若使用 Compose 的 .env 文件,也需要在服务中通过 environment 引用变量或通过 env_file 传入。

代理、安全与验证

- 检查的是 req.socket.remoteAddress,不读取 X-Forwarded-For、X-Real-IP 或 Forwarded。Docker/NAT/反向代理下,该地址可能是网关或代理 IP,而不是浏览器所在机器的 IP。
- 放行代理 IP 会放行经该代理转发的所有客户端;本机代理也可能将外部请求转发为回环连接。因此,代理后的客户端限制应在代理层执行。“仅本地”指服务端(或容器内)的回环连接,不等于只允许本机浏览器。
-  会放开读取历史、修改配置、执行 hook 等敏感接口的来源 IP 限制;IP 白名单不是身份认证,请用可信网络或外部认证保护这些接口,不要直接暴露到不可信网络。
- 此变量只影响 /dsh-hooks/*,不会改变服务监听地址、端口、防火墙规则或其他插件的权限。

可从允许及不允许的客户端分别请求 GET /dsh-hooks/status(主机和端口替换为实际地址)。通过本插件的检查时返回正常状态 JSON;被本插件拒绝时返回 HTTP 403:
json
{"ok":false,"error":{"code":"forbidden","message":"IP not allowed"}}

若白名单配置后仍收到该错误,先确认变量已传入实际服务进程,再检查服务端看到的是客户端 IP 还是代理/网关 IP。若连接超时或被拒绝连接,则还需检查监听地址、端口映射和网络规则。

通用 webhook 示例

除了飞书,examples/notify-webhook.mjs 把完整 hook 上下文作为一份 JSON POST 到任意 HTTP 端点——Slack 入站 webhook、Discord、企业微信/钉钉自定义机器人、ntfy、Bark、n8n 都能接:
yaml
- id: dsh-hooks
name: dsh-hooks
config:
hooks:
- on: 'turn/end'
when: 'completed'
run: 'node examples/notify-webhook.mjs --url https://hooks.slack.com/services/…'
- on: 'tool/result'        # 工具连续失败时告警
run: 'node examples/notify-webhook.mjs --slack'

URL 也可放在 dsh 进程环境的 DSH_HOOKS_WEBHOOK_URL(不要写进配置文件)。--slack 把 payload 换成一行摘要的 { text } 格式;--timeout  控制超时(默认 10000,传输失败自动重试一次)。

飞书通知示例

两种接入方式任选:Web GUI 扫码(推荐,无需终端)或 setup CLI——扫码自动创建飞书应用并写好全部 hook 配置。

方式一:Web GUI 扫码

打开 dsh web 设置 → 「Hooks」分区 → 「飞书通知」,填好 profile(默认 web)点「扫码连接飞书」:

1. 面板内显示飞书授权二维码(含有效期倒计时)
2. 用飞书扫码,自动创建名为「DSH 通知机器人」的应用(仅 im:message:send_as_bot 权限),扫码者本人为通知接收人
3. 连接完成后显示应用摘要,可「发送测试卡片」验证、直接修改卡片截断长度(50–5000 字符,默认 300,即时生效),「重新连接」可换绑新应用
4. 重启 dsh web 生效

方式二:setup CLI
sh
dsh-hooks feishu-setup                 # 默认 profile:web
dsh-hooks feishu-setup --profile work  # 指定其他 profile
dsh-hooks feishu-test                  # 用已存凭据发送测试卡片验证

feishu-setup 会打印二维码(并在浏览器中打开),等你用飞书扫码后,自动创建名为「DSH 通知机器人」的应用(带消息发送权限)。

两种方式写入的文件相同:

| 文件 | 用途 |
| --- | --- |
| ~/.dsh/dsh-hooks/feishu-config.json | app id/secret 与你的 open_id(通知目标),权限 0600,严禁提交;result_max_chars 控制卡片内容截断长度(默认 300,可在 Web GUI 中修改) |
| ~/.dsh/dsh-hooks/notify-feishu.mjs | hook 引用的通知脚本稳定副本 |
| ~/.dsh/profiles//cordis.patch.yml | dsh-hooks 配置块:turn/end(completed/error/aborted)+ approval/asked + agent/error 卡片 hook |

完成后重启 dsh web——回合结束、请求审批、agent 出错时就会收到卡片通知。

飞书卡片示例

方式三:手动配置

想自己接线?见 examples/notify-feishu.mjs——零依赖脚本,通过飞书应用 API(不需要群自定义机器人)发送回合完成 / 审批通知。配置示例:
yaml
- id: dsh-hooks
name: dsh-hooks
config:
hooks:
- on: 'turn/end'
when: 'completed'
run: 'node D:/path/to/examples/notify-feishu.mjs'
- on: 'approval/asked'
run: 'node D:/path/to/examples/notify-feishu.mjs --approval'

同时在 dsh 进程环境中提供 DSH_HOOKS_FEISHU_APP_ID / DSH_HOOKS_FEISHU_APP_SECRET / DSH_HOOKS_FEISHU_TO(绝不能写进配置文件)。

安全

Hook 会以 dsh 进程的权限执行任意命令,只配置你信任的命令。Secret 放环境变量或 dsh 凭据存储——永远不要写进 cordis.patch.yml。

设计

遵循 dsh 插件约定:dsh.bundle.patch 挂载插件行;插件监听持久 session/event firehose 与 agent 生命周期事件;发射是不可逆副作用,补偿而非阻塞(失败仅警告、绝不重试)。

开发
sh
pnpm install
pnpm run check     # typecheck + test + build

发布与 CI 运维(Trusted Publishing、安全扫描、踩坑记录):见 docs/RELEASING.md。

License

MIT

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入社群

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

DPharness QQ 群二维码,QQ 扫码进群
QQ 扫码进群
DPharness 飞书群二维码,飞书扫码进群
飞书扫码进群