dsh-hooks 0.10.0 → 0.12.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 +107 -10
- package/README.zh.md +107 -10
- package/lib/client.js +87 -6
- package/lib/config.d.ts +46 -7
- package/lib/config.js +18 -2
- package/lib/context.d.ts +15 -1
- package/lib/context.js +8 -0
- package/lib/dry-run.js +35 -6
- package/lib/events.d.ts +21 -15
- package/lib/events.js +100 -15
- package/lib/history.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +140 -28
- package/lib/patch-config.d.ts +4 -0
- package/lib/patch-config.js +5 -1
- package/lib/runner.d.ts +12 -1
- package/lib/runner.js +42 -4
- package/lib/server.d.ts +5 -1
- package/lib/server.js +20 -1
- package/lib/usage.d.ts +79 -0
- package/lib/usage.js +110 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -54,6 +54,18 @@ Add a config block to your profile's `cordis.patch.yml`:
|
|
|
54
54
|
channel: 'webhook' # POST JSON to any HTTP endpoint
|
|
55
55
|
url: 'https://hooks.slack.com/services/…'
|
|
56
56
|
slack: true # optional: { text } one-line summary (Slack style)
|
|
57
|
+
- on: 'step/end'
|
|
58
|
+
run: 'node examples/log-step.mjs'
|
|
59
|
+
debounceMs: 500 # optional: debounce high-frequency events
|
|
60
|
+
maxConcurrent: 2 # optional: cap concurrent processes
|
|
61
|
+
- on: 'tool/result'
|
|
62
|
+
match:
|
|
63
|
+
toolDurationMs: '>10000' # numeric comparison ({ gt: 10000 } object form too)
|
|
64
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
65
|
+
- on: 'turn/end'
|
|
66
|
+
enabled: false # optional: disable without deleting
|
|
67
|
+
cwd: 'session' # optional: run in the session working directory
|
|
68
|
+
run: 'node examples/log-turn.mjs'
|
|
57
69
|
```
|
|
58
70
|
|
|
59
71
|
Every hook field:
|
|
@@ -62,24 +74,28 @@ Every hook field:
|
|
|
62
74
|
| --- | --- | --- |
|
|
63
75
|
| `on` | triggering event (see the event table) | required |
|
|
64
76
|
| `when` | filter `turn/end` by end reason | all reasons |
|
|
65
|
-
| `match` | field → regex, all must match; fields are context keys (`tool` / `sessionName` / `sessionId` / `error` / `source` / `cwd` / `content` / `reason`, …), a field absent from the context never matches | no filter |
|
|
77
|
+
| `match` | field → regex or numeric comparison, all must match; fields are context keys (`tool` / `sessionName` / `sessionId` / `error` / `source` / `cwd` / `content` / `reason` / `turn` / `durationMs` / `toolDurationMs`, …), a field absent from the context never matches. Regexes test the string form; comparisons (`{ gt: 10000 }` or `'>10000'`, ops `gt` / `gte` / `lt` / `lte` / `eq`, combinable) apply only to numeric fields and never match non-numeric ones | no filter |
|
|
66
78
|
| `run` | command spawned through the platform shell (exactly one of `run` / `notify`) | one of the two required |
|
|
67
79
|
| `notify` | built-in notification (exactly one of `run` / `notify`): `channel: webhook` (HTTP JSON; omit `url` to use `DSH_HOOKS_WEBHOOK_URL`, `slack: true` for a one-line summary) or `channel: desktop` (platform balloon/toast) | one of the two required |
|
|
68
80
|
| `input` | `env` passes only the `DSH_HOOK_*` variables; `stdin` additionally writes the full context JSON to the command's stdin | `env` |
|
|
69
81
|
| `timeoutMs` | per-run timeout (ms); the process tree is terminated on expiry | 10000 |
|
|
70
82
|
| `retries` | retry count for non-zero exit codes (spawn failures and timeouts never retry) | 0 |
|
|
71
83
|
| `retryDelayMs` | base delay between retries (ms), doubles per attempt | 500 |
|
|
84
|
+
| `enabled` | `false` disables the hook without deleting it: the declaration stays, dispatch skips it silently (never counts as a failure) | `true` |
|
|
85
|
+
| `cwd` | working directory for the spawned command: `session` runs in the session's cwd, an absolute path runs there (`run` only) | plugin process directory |
|
|
86
|
+
| `maxConcurrent` | max concurrently running processes for this hook; triggers beyond the cap are dropped (recorded as `skipped`) | unlimited |
|
|
87
|
+
| `debounceMs` | debounce window (ms): triggers of high-frequency events (`step/end`, `tool/*`, …) inside the window collapse into one trailing execution carrying the latest context | 0 (off) |
|
|
72
88
|
|
|
73
89
|
## Events (v1)
|
|
74
90
|
|
|
75
91
|
| Event | When it fires | Useful context |
|
|
76
92
|
| --- | --- | --- |
|
|
77
|
-
| `turn/start` | A turn begins | session id, turn |
|
|
93
|
+
| `turn/start` | A turn begins (with `turn/start` hooks, dispatch waits for the turn's first direct user message and attaches its text as `DSH_HOOK_CONTENT`; turns without one dispatch content-less at `turn/end`, see below) | session id, turn, initiating message text |
|
|
78
94
|
| `turn/end` | A turn ends (`completed` / `error` / `aborted` / `blocked` / `max-tokens` / `interrupted`) | reason, turn, duration, content, turn token usage, running subagents |
|
|
79
95
|
| `tree/settled` | A watched session's whole subagent tree settles (no live child still running) after a turn ended with work handed off | total subagents, handoff→settle duration |
|
|
80
96
|
| `step/end` | One step of a turn ends (one model call plus its tool executions) | turn, step |
|
|
81
97
|
| `tool/call` | The model requests one tool invocation | tool name, call id, raw arguments JSON |
|
|
82
|
-
| `tool/result` | A tool call completes | tool name (resolved), result text, failure identity |
|
|
98
|
+
| `tool/result` | A tool call completes | tool name (resolved), result text, failure identity, wall-clock duration (absent when the pairing call was never seen) |
|
|
83
99
|
| `user/message` | A user-role message appears on the surface | source kind (`user` / `plugin` / …), message text |
|
|
84
100
|
| `approval/asked` | A tool call requests user approval | tool name, call id, approval id, reason |
|
|
85
101
|
| `approval/decided` | A pending approval gets its outcome (paired with `approval/asked` by id) | outcome, tool name (resolved), call id, approval id |
|
|
@@ -91,6 +107,7 @@ Every hook field:
|
|
|
91
107
|
| `agent/error` | The agent loop reports an error | error text |
|
|
92
108
|
| `agent/status` | Agent status transition | status |
|
|
93
109
|
| `hook/failed` | A hook fails consecutively past `failedAlertThreshold` (default 3; synthetic, emitted from the outcome stream) | failing hook summary, consecutive failure count |
|
|
110
|
+
| `usage/daily` | The first event after the local calendar day rolls over (synthetic, no timers): reports the token usage of the day that just ended | covered day, turns that day, contributing sessions, day's token totals |
|
|
94
111
|
|
|
95
112
|
The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `error`, …). Hooks for other events run unconditionally.
|
|
96
113
|
|
|
@@ -112,16 +129,17 @@ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `
|
|
|
112
129
|
| `DSH_HOOK_CALL_ID` | tool call id (approval / tool events) |
|
|
113
130
|
| `DSH_HOOK_TOOL_ARGS` | raw tool arguments JSON (tool/call) |
|
|
114
131
|
| `DSH_HOOK_TOOL_ERROR` | tool failure identity `name: code` (tool/result errors) |
|
|
132
|
+
| `DSH_HOOK_TOOL_DURATION_MS` | wall-clock tool execution ms (tool/result; absent when the pairing tool/call was never seen) |
|
|
115
133
|
| `DSH_HOOK_SOURCE` | message / title source kind (`user`, `plugin`, `fallback`, `provider`, …) |
|
|
116
134
|
| `DSH_HOOK_DURATION_MS` | turn duration ms (turn/end) |
|
|
117
135
|
| `DSH_HOOK_STATUS` | agent status (`agent/status`) |
|
|
118
136
|
| `DSH_HOOK_ERROR` | error text (`agent/error`, and the failure message on `turn/end` error) |
|
|
119
|
-
| `DSH_HOOK_CONTENT` | event content snapshot: turn assistant text, tool result text, user message text |
|
|
120
|
-
| `DSH_HOOK_USAGE_INPUT_TOKENS` |
|
|
121
|
-
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` |
|
|
122
|
-
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` |
|
|
123
|
-
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` |
|
|
124
|
-
| `DSH_HOOK_USAGE_REASONING_TOKENS` |
|
|
137
|
+
| `DSH_HOOK_CONTENT` | event content snapshot: turn assistant text, tool result text, user message text, turn-initiating message text (turn/start) |
|
|
138
|
+
| `DSH_HOOK_USAGE_INPUT_TOKENS` | input token total (turn/end: this turn, summed across steps; usage/daily: the whole day) |
|
|
139
|
+
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` | output token total (same scoping) |
|
|
140
|
+
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` | cache-read tokens when reported (same scoping) |
|
|
141
|
+
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` | cache-write tokens when reported (same scoping) |
|
|
142
|
+
| `DSH_HOOK_USAGE_REASONING_TOKENS` | reasoning tokens when reported (same scoping) |
|
|
125
143
|
| `DSH_HOOK_RUNNING_SUBAGENTS` | live subagents still running under this session (turn/end; `0` = none — lets a hook tell "work handed off to background subagents" apart from "the turn finished for real") |
|
|
126
144
|
| `DSH_HOOK_PARENT_SESSION_ID` | parent session id (subagent lineage; absent for top-level sessions) |
|
|
127
145
|
| `DSH_HOOK_SUBAGENT` | `1` when the session is a subagent child, `0` otherwise |
|
|
@@ -134,6 +152,9 @@ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `
|
|
|
134
152
|
| `DSH_HOOK_TREE_DURATION_MS` | parent turn/end → tree settle duration, ms (`tree/settled`) |
|
|
135
153
|
| `DSH_HOOK_FAILED_HOOK` | identity summary of the hook that failed consecutively (`hook/failed`) |
|
|
136
154
|
| `DSH_HOOK_FAILURES` | consecutive failure count when the alert fired (`hook/failed`) |
|
|
155
|
+
| `DSH_HOOK_USAGE_DAY` | local calendar day the token totals cover, `YYYY-MM-DD` (`usage/daily`) |
|
|
156
|
+
| `DSH_HOOK_USAGE_TURNS` | turns with reported accounting that day (`usage/daily`) |
|
|
157
|
+
| `DSH_HOOK_USAGE_SESSIONS` | distinct sessions that contributed usage that day (`usage/daily`) |
|
|
137
158
|
| `DSH_HOOK_TIMESTAMP` | ISO timestamp |
|
|
138
159
|
|
|
139
160
|
- `{{var}}` placeholders inside `run` are substituted from the same context, e.g. `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`.
|
|
@@ -167,6 +188,82 @@ For the simpler "notify only once the whole tree settles" pattern, the synthetic
|
|
|
167
188
|
|
|
168
189
|
Settled-but-idle continuable children do not count as running, so they don't keep suppressing the notification. The settle watch is event-driven and best-effort: it survives until the plugin restarts, and a failed re-check drops the watch silently (no late notification).
|
|
169
190
|
|
|
191
|
+
### usage/daily: the cross-day token report
|
|
192
|
+
|
|
193
|
+
`turn/end` answers "what did this turn cost". For a per-day view, use the synthetic `usage/daily` event: the plugin accumulates every reported `turn/end` usage in memory per **local calendar day** (subagent sessions included — same account), and when the day rolls over it emits one report for the day that just ended, on the next event that arrives. Detection is purely event-driven: no timers, no scheduled tasks.
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
- on: 'usage/daily'
|
|
197
|
+
match: { usageInputTokens: '>0' } # optional: skip days without usage
|
|
198
|
+
run: 'node examples/log-usage.mjs' # or notify: { channel: 'webhook', url: '…' }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`DSH_HOOK_USAGE_DAY` is the day the report covers (`YYYY-MM-DD`); `DSH_HOOK_USAGE_TURNS` / `DSH_HOOK_USAGE_SESSIONS` are that day's counted turns and contributing sessions; the token details reuse the `turn/end` variable names (`usageInputTokens` / `usageOutputTokens` / `usageCacheReadTokens` / `usageCacheWriteTokens` / `usageReasoningTokens`) with day scope instead of turn scope.
|
|
202
|
+
|
|
203
|
+
Three boundaries by design, not bugs:
|
|
204
|
+
|
|
205
|
+
- **In-memory**: a plugin-process restart drops the day in progress (the new process starts a fresh day at zero); reports already emitted are unaffected.
|
|
206
|
+
- **Event-driven, not timed**: a day is reported when the next event arrives, so after a quiet midnight the report waits for the next event; a day with no reported turn usage is never reported (an empty report is noise).
|
|
207
|
+
- **Zero cost when unused**: with no `usage/daily` hook declared, no accumulation and no day check happen at all.
|
|
208
|
+
|
|
209
|
+
`dsh-hooks dry-run usage/daily` simulates a report for "yesterday" with non-zero tokens, so match filters and the command can be verified first.
|
|
210
|
+
|
|
211
|
+
### Numeric match comparisons
|
|
212
|
+
|
|
213
|
+
Numeric context fields (`turn`, `step`, `durationMs`, `toolDurationMs`, `usage*`, `runningSubagents`, …) support real comparisons instead of regex hacks:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
- on: 'tool/result'
|
|
217
|
+
match: { toolDurationMs: '>10000' } # string syntax: > >= < <= =
|
|
218
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
219
|
+
|
|
220
|
+
- on: 'tool/result'
|
|
221
|
+
match:
|
|
222
|
+
toolDurationMs: { gt: 10000, lt: 60000 } # object syntax: gt/gte/lt/lte/eq, combinable
|
|
223
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Rules:
|
|
227
|
+
|
|
228
|
+
- Comparison semantics apply only to **numeric** fields; on a string field a comparison **never matches** (no string coercion).
|
|
229
|
+
- A string value counts as a comparison only when it starts with `>` / `>=` / `<` / `<=` / `=` followed by a number (e.g. `'>10000'`); anything else stays a plain regex.
|
|
230
|
+
- A missing field still never matches. An empty object `{}` matches vacuously.
|
|
231
|
+
|
|
232
|
+
### Execution options: enabled / cwd / maxConcurrent / debounceMs
|
|
233
|
+
|
|
234
|
+
Every hook can tune its execution independently:
|
|
235
|
+
|
|
236
|
+
- **`enabled: false`** disables the hook but keeps the declaration. Skipping is silent — no history record, never part of a failure streak (`hook/failed` never fires for a disabled hook). dry-run marks it `enabled: false(已停用)`.
|
|
237
|
+
- **`cwd: 'session'`** spawns `run` in the session's working directory (the project the agent works on), so hook scripts can read/write project files directly; an absolute path works too. Defaults to the plugin process directory.
|
|
238
|
+
- **`maxConcurrent`** caps concurrent processes for the hook. Triggers beyond the cap are dropped and recorded as `skipped` (no failure alert); one logical run (its internal retries included) always occupies one slot.
|
|
239
|
+
- **`debounceMs`** debounces high-frequency events (`step/end`, `tool/*`, …): triggers inside the window collapse into one **trailing** execution carrying the latest context. Collapsed triggers are fully silent — they never flood the log or history. New triggers after the window run normally.
|
|
240
|
+
|
|
241
|
+
The recommended combination against `step/end` / `tool/*` spawn storms:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
- on: 'step/end'
|
|
245
|
+
run: 'node examples/log-step.mjs'
|
|
246
|
+
debounceMs: 500 # consecutive step ends within half a second run once
|
|
247
|
+
maxConcurrent: 2 # safety net: at most 2 processes even when slow
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### turn/start carries the initiating message
|
|
251
|
+
|
|
252
|
+
The session log records `turn/start` *before* the turn's `user/message`, so the prompt text is not readable at turn-start time. When `turn/start` hooks exist, the plugin defers their dispatch until the turn's first direct user message is classified, attaching its text as `DSH_HOOK_CONTENT` (capped at 2000 chars):
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
- on: 'turn/start'
|
|
256
|
+
match: { content: 'deploy|release' } # only turns asking about deploys
|
|
257
|
+
notify: { channel: 'desktop' }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Timing notes:
|
|
261
|
+
|
|
262
|
+
- The deferral only kicks in when `turn/start` hooks exist; otherwise dispatch stays as before (immediate, no content).
|
|
263
|
+
- Only direct user messages (`source.kind === 'user'`) complete the dispatch; synthetic injections (agent/plugin sources) do not.
|
|
264
|
+
- A turn without a direct user message (e.g. a goal continuation round) dispatches `turn/start` **without content** at `turn/end`; a new turn flushes an unclaimed previous `turn/start` first.
|
|
265
|
+
- For direct-user turns the delay is typically milliseconds (`user/message` immediately follows `turn/start`), still ahead of any step/tool events.
|
|
266
|
+
|
|
170
267
|
## Generic webhook example
|
|
171
268
|
|
|
172
269
|
Besides Feishu, `examples/notify-webhook.mjs` posts the full hook context as one JSON document to any HTTP endpoint — Slack incoming webhooks, Discord, Lark/DingTalk custom bots, ntfy, Bark, n8n:
|
|
@@ -200,7 +297,7 @@ Every hook trigger is recorded into an in-memory ring buffer (default 500 entrie
|
|
|
200
297
|
hooks: […]
|
|
201
298
|
```
|
|
202
299
|
|
|
203
|
-
Each record: timestamp, kind (run/notify), event, command, session, outcome (spawned / exit-0 / exit-nonzero / timeout / sent / send-failed, …), exit code, duration, stderr tail. Disk failures are swallowed silently — history never blocks a hook.
|
|
300
|
+
Each record: timestamp, kind (run/notify), event, command, session, outcome (spawned / exit-0 / exit-nonzero / timeout / skipped / sent / send-failed, …), exit code, duration, stderr tail. Disk failures are swallowed silently — history never blocks a hook.
|
|
204
301
|
|
|
205
302
|
## dry-run: verify config
|
|
206
303
|
|
package/README.zh.md
CHANGED
|
@@ -54,6 +54,18 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
54
54
|
channel: 'webhook' # POST JSON 到任意 HTTP 端点
|
|
55
55
|
url: 'https://hooks.slack.com/services/…'
|
|
56
56
|
slack: true # 可选:改为 { text } 单行摘要(Slack 风格)
|
|
57
|
+
- on: 'step/end'
|
|
58
|
+
run: 'node examples/log-step.mjs'
|
|
59
|
+
debounceMs: 500 # 可选:高频事件去抖,窗口内合并为一次
|
|
60
|
+
maxConcurrent: 2 # 可选:并发上限,超出的触发被丢弃
|
|
61
|
+
- on: 'tool/result'
|
|
62
|
+
match:
|
|
63
|
+
toolDurationMs: '>10000' # 数值比较(也支持 { gt: 10000 } 对象语法)
|
|
64
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
65
|
+
- on: 'turn/end'
|
|
66
|
+
enabled: false # 可选:停用但不删除(跳过派发)
|
|
67
|
+
cwd: 'session' # 可选:在会话工作目录执行
|
|
68
|
+
run: 'node examples/log-turn.mjs'
|
|
57
69
|
```
|
|
58
70
|
|
|
59
71
|
每个 hook 的完整字段:
|
|
@@ -62,24 +74,28 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
62
74
|
| --- | --- | --- |
|
|
63
75
|
| `on` | 触发事件(见上方事件表) | 必填 |
|
|
64
76
|
| `when` | 对 `turn/end` 按结束原因过滤 | 全部原因 |
|
|
65
|
-
| `match` | 字段 →
|
|
77
|
+
| `match` | 字段 → 正则或数值比较,全部匹配才触发;字段为上下文键(`tool`/`sessionName`/`sessionId`/`error`/`source`/`cwd`/`content`/`reason`/`turn`/`durationMs`/`toolDurationMs`…),上下文中不存在的字段视为不匹配。正则匹配字段的字符串表示;数值比较(`{ gt: 10000 }` 或 `'>10000'`,支持 `gt`/`gte`/`lt`/`lte`/`eq` 组合)只对数字字段生效,非数字字段上的比较永不匹配 | 不过滤 |
|
|
66
78
|
| `run` | 通过系统 shell 执行的命令(与 `notify` 二选一) | 二选一必填 |
|
|
67
79
|
| `notify` | 内置通知(与 `run` 二选一):`channel: webhook`(HTTP JSON,`url` 可省略用 `DSH_HOOKS_WEBHOOK_URL`,`slack: true` 换单行摘要)或 `channel: desktop`(系统气泡/toast) | 二选一必填 |
|
|
68
80
|
| `input` | `env` 只传 `DSH_HOOK_*` 环境变量;`stdin` 额外把完整上下文 JSON 写入命令标准输入 | `env` |
|
|
69
81
|
| `timeoutMs` | 单次执行超时(毫秒),超时终止进程树 | 10000 |
|
|
70
82
|
| `retries` | 非零退出码的重试次数(spawn 失败与超时不重试) | 0 |
|
|
71
83
|
| `retryDelayMs` | 重试基础间隔(毫秒),每次翻倍 | 500 |
|
|
84
|
+
| `enabled` | `false` 停用该 hook:配置保留、静默跳过派发(不计入失败) | `true` |
|
|
85
|
+
| `cwd` | 命令执行目录:`session` 在会话工作目录执行;绝对路径在指定目录执行(只作用于 `run`) | 插件进程目录 |
|
|
86
|
+
| `maxConcurrent` | 该 hook 允许的最大并发进程数;超出的触发被丢弃(执行历史记 `skipped`) | 不限 |
|
|
87
|
+
| `debounceMs` | 去抖窗口(毫秒):高频事件(`step/end`、`tool/*`…)窗口内的多次触发合并为一次 trailing 执行,携带最新上下文 | 0(不去抖) |
|
|
72
88
|
|
|
73
89
|
## 事件(v1)
|
|
74
90
|
|
|
75
91
|
| 事件 | 触发时机 | 有用上下文 |
|
|
76
92
|
| --- | --- | --- |
|
|
77
|
-
| `turn/start` |
|
|
93
|
+
| `turn/start` | 回合开始(若有 `turn/start` hook,派发延迟到本回合的首条用户消息分类后,把触发文本注入 `DSH_HOOK_CONTENT`;无用户消息的回合在 `turn/end` 时无内容派发,见下方说明) | 会话 id、回合号、触发消息文本 |
|
|
78
94
|
| `turn/end` | 回合结束(`completed` / `error` / `aborted` / `blocked` / `max-tokens` / `interrupted`) | reason、回合号、耗时、内容、本回合 token 用量、运行中子代理数 |
|
|
79
95
|
| `tree/settled` | 回合结束后把工作交给子代理的会话,其整个子代理树全部落定(无存活子代理仍在运行) | 子代理总数、交接到落定的耗时 |
|
|
80
96
|
| `step/end` | 回合内一步结束(一次模型调用 + 其工具执行) | 回合号、步号 |
|
|
81
97
|
| `tool/call` | 模型请求一次工具调用 | 工具名、调用 id、原始参数 JSON |
|
|
82
|
-
| `tool/result` | 工具调用完成 |
|
|
98
|
+
| `tool/result` | 工具调用完成 | 工具名(自动反查)、结果文本、失败标识、工具耗时(配对失败时无耗时) |
|
|
83
99
|
| `user/message` | 会话表面出现用户角色消息 | 来源 kind(`user` / `plugin` / …)、消息文本 |
|
|
84
100
|
| `approval/asked` | 工具调用请求用户审批 | 工具名、调用 id、审批 id、原因 |
|
|
85
101
|
| `approval/decided` | 待审批项得出结果(与 `approval/asked` 按 id 配对) | 结果 outcome、工具名(自动反查)、调用 id、审批 id |
|
|
@@ -91,6 +107,7 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
91
107
|
| `agent/error` | Agent 循环报错 | 错误文本 |
|
|
92
108
|
| `agent/status` | Agent 状态切换 | 状态 |
|
|
93
109
|
| `hook/failed` | 同一 hook 连续失败达到 `failedAlertThreshold`(默认 3;合成事件,从结果流发射) | 失败 hook 摘要、连续失败次数 |
|
|
110
|
+
| `usage/daily` | 本地日历日翻篇后的下一个事件(合成事件,无定时器):报告刚结束那一天的 token 用量 | 覆盖日期、当日回合数、贡献会话数、当日 token 明细 |
|
|
94
111
|
|
|
95
112
|
`turn/end` 的 `when` 匹配结束原因(`completed`、`error`…);其他事件的 hook 无条件执行。
|
|
96
113
|
|
|
@@ -112,16 +129,17 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
112
129
|
| `DSH_HOOK_CALL_ID` | 工具调用 id(审批 / 工具事件) |
|
|
113
130
|
| `DSH_HOOK_TOOL_ARGS` | 工具原始参数 JSON(tool/call) |
|
|
114
131
|
| `DSH_HOOK_TOOL_ERROR` | 工具失败标识 `名称: 代码`(tool/result 出错时) |
|
|
132
|
+
| `DSH_HOOK_TOOL_DURATION_MS` | 工具执行耗时毫秒(tool/result;配对 tool/call 丢失时无此变量) |
|
|
115
133
|
| `DSH_HOOK_SOURCE` | 消息 / 标题来源 kind(`user`、`plugin`、`fallback`、`provider`…) |
|
|
116
134
|
| `DSH_HOOK_DURATION_MS` | 回合耗时毫秒(turn/end) |
|
|
117
135
|
| `DSH_HOOK_STATUS` | Agent 状态(agent/status) |
|
|
118
136
|
| `DSH_HOOK_ERROR` | 错误文本(agent/error,以及 turn/end 出错时的失败详情) |
|
|
119
|
-
| `DSH_HOOK_CONTENT` |
|
|
120
|
-
| `DSH_HOOK_USAGE_INPUT_TOKENS` |
|
|
121
|
-
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` |
|
|
122
|
-
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` |
|
|
123
|
-
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` |
|
|
124
|
-
| `DSH_HOOK_USAGE_REASONING_TOKENS` |
|
|
137
|
+
| `DSH_HOOK_CONTENT` | 事件内容快照:回合最后助手文本、工具结果文本、用户消息文本、回合触发消息文本(turn/start) |
|
|
138
|
+
| `DSH_HOOK_USAGE_INPUT_TOKENS` | 输入 token 总量(turn/end 为本回合、逐 step 聚合;usage/daily 为当日聚合) |
|
|
139
|
+
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` | 输出 token 总量(同上) |
|
|
140
|
+
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` | 缓存读 token(有上报时,同上) |
|
|
141
|
+
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` | 缓存写 token(有上报时,同上) |
|
|
142
|
+
| `DSH_HOOK_USAGE_REASONING_TOKENS` | 思考 token(有上报时,同上) |
|
|
125
143
|
| `DSH_HOOK_RUNNING_SUBAGENTS` | 本会话下仍在运行的存活子代理数(turn/end;`0` = 无——让 hook 能区分「工作已交给后台子代理」与「回合真正结束」) |
|
|
126
144
|
| `DSH_HOOK_PARENT_SESSION_ID` | 父会话 id(子代理谱系;顶层会话无此变量) |
|
|
127
145
|
| `DSH_HOOK_SUBAGENT` | 会话为子代理时为 `1`,否则 `0` |
|
|
@@ -134,6 +152,9 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
134
152
|
| `DSH_HOOK_TREE_DURATION_MS` | 父回合结束 → 树落定的耗时(毫秒,`tree/settled`) |
|
|
135
153
|
| `DSH_HOOK_FAILED_HOOK` | 连续失败的 hook 身份摘要(`hook/failed`) |
|
|
136
154
|
| `DSH_HOOK_FAILURES` | 告警触发时的连续失败次数(`hook/failed`) |
|
|
155
|
+
| `DSH_HOOK_USAGE_DAY` | 报告覆盖的本地日历日 `YYYY-MM-DD`(`usage/daily`) |
|
|
156
|
+
| `DSH_HOOK_USAGE_TURNS` | 当日计入的回合数(`usage/daily`) |
|
|
157
|
+
| `DSH_HOOK_USAGE_SESSIONS` | 当日贡献用量的会话数(`usage/daily`) |
|
|
137
158
|
| `DSH_HOOK_TIMESTAMP` | ISO 时间戳 |
|
|
138
159
|
|
|
139
160
|
- `run` 里的 `{{变量}}` 占位符会从同一上下文替换,例如 `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`。
|
|
@@ -168,6 +189,82 @@ config:
|
|
|
168
189
|
|
|
169
190
|
已落定但闲置(idle)的 continuable 子代理不计入运行中,不会一直压住通知。落定监视是事件驱动且 best-effort 的:插件重启后监视集合丢失;重查失败会静默放弃该监视(不会补发迟到的通知)。
|
|
170
191
|
|
|
192
|
+
### usage/daily:跨日 token 日报
|
|
193
|
+
|
|
194
|
+
`turn/end` 只回答「这个回合花了多少」。要按天看成本,用合成事件 `usage/daily`:插件在内存里按**本地日历日**累计每个 `turn/end` 上报的 token(子代理会话的回合一并计入——同一个账号),日期翻篇后对下一个到达的事件发射一次日报,报告刚结束的那一天。检测纯事件驱动、无定时器、无定时任务。
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
- on: 'usage/daily'
|
|
198
|
+
match: { usageInputTokens: '>0' } # 可选:跳过没有用量的日子
|
|
199
|
+
run: 'node examples/log-usage.mjs' # 或 notify: { channel: 'webhook', url: '…' }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`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`),语义变为「该日聚合」。
|
|
203
|
+
|
|
204
|
+
三条边界(按设计,不是 bug):
|
|
205
|
+
|
|
206
|
+
- **内存累计**:插件进程重启会丢掉进行中那一天的累计(重启后从新的一天、从零开始);已发出的日报不受影响。
|
|
207
|
+
- **事件驱动而非定时**:一天的用量要等下一个事件到达才报告,所以跨夜后若一直没动静,日报会推迟到下一次有事件时补发;那一天从未有回合上报用量则不发射(空日报是噪声)。
|
|
208
|
+
- **零开销**:没有声明任何 `usage/daily` hook 时,插件完全不做累计与跨日检测。
|
|
209
|
+
|
|
210
|
+
`dsh-hooks dry-run usage/daily` 用「昨天」和非零 token 模拟一次日报,可先验证 match 与命令。
|
|
211
|
+
|
|
212
|
+
### match 数值比较
|
|
213
|
+
|
|
214
|
+
对数字字段(`turn`、`step`、`durationMs`、`toolDurationMs`、`usage*`、`runningSubagents`…)可以直接写数值比较,不用绕正则:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
- on: 'tool/result'
|
|
218
|
+
match: { toolDurationMs: '>10000' } # 字符串语法:> >= < <= =
|
|
219
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
220
|
+
|
|
221
|
+
- on: 'tool/result'
|
|
222
|
+
match:
|
|
223
|
+
toolDurationMs: { gt: 10000, lt: 60000 } # 对象语法:gt/gte/lt/lte/eq 可组合
|
|
224
|
+
run: 'node examples/notify-slow-tool.mjs'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
规则:
|
|
228
|
+
|
|
229
|
+
- 比较语义只对**数字字段**生效;字段是字符串时比较**永不匹配**(不会把字符串转数字强比)。
|
|
230
|
+
- 字符串语法以 `>`/`>=`/`<`/`<=`/`=` 开头且后跟数字才算比较(如 `'>10000'`);其余字符串仍是普通正则。
|
|
231
|
+
- 字段缺失照旧视为不匹配。空对象 `{}` 恒真(无任何条件)。
|
|
232
|
+
|
|
233
|
+
### 执行选项:enabled / cwd / maxConcurrent / debounceMs
|
|
234
|
+
|
|
235
|
+
每个 hook 都可以独立微调执行方式:
|
|
236
|
+
|
|
237
|
+
- **`enabled: false`**:停用该 hook 但保留配置。跳过是静默的——不记执行历史、不计入失败链(`hook/failed` 不会因停用的 hook 触发)。dry-run 会标出 `enabled: false(已停用)`。
|
|
238
|
+
- **`cwd: 'session'`**:在会话工作目录(agent 正在工作的项目目录)执行 `run`,方便 hook 脚本直接读写当前项目文件;`cwd` 也接受绝对路径。缺省在插件进程目录执行。
|
|
239
|
+
- **`maxConcurrent`**:并发进程上限。超过上限的触发被丢弃并记入执行历史(`skipped`,不触发失败告警);一次逻辑执行(含其内部重试)始终占用一个名额。
|
|
240
|
+
- **`debounceMs`**:去抖窗口。高频事件(`step/end`、`tool/*`…)窗口内的多次触发合并为一次 **trailing** 执行,携带最新一次的上下文;被合并掉的触发完全静默,不会刷日志/历史。窗口结束后的新触发正常执行。
|
|
241
|
+
|
|
242
|
+
防 `step/end`/`tool/*` spawn 风暴的推荐组合:
|
|
243
|
+
|
|
244
|
+
```yaml
|
|
245
|
+
- on: 'step/end'
|
|
246
|
+
run: 'node examples/log-step.mjs'
|
|
247
|
+
debounceMs: 500 # 半秒内的连续步结束只跑一次
|
|
248
|
+
maxConcurrent: 2 # 兜底:命令变慢时并发不超过 2
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### turn/start 的触发内容
|
|
252
|
+
|
|
253
|
+
会话日志先记录 `turn/start`、后记录该回合的 `user/message`,所以回合开始那一刻还读不到触发文本。插件因此把 `turn/start` 的派发**延迟到本回合首条直接用户消息分类后**,把消息文本注入 `DSH_HOOK_CONTENT`(截断 2000 字符):
|
|
254
|
+
|
|
255
|
+
```yaml
|
|
256
|
+
- on: 'turn/start'
|
|
257
|
+
match: { content: '部署|发版' } # 只关心包含关键词的回合
|
|
258
|
+
notify: { channel: 'desktop' }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
时序说明:
|
|
262
|
+
|
|
263
|
+
- 有 `turn/start` hook 时才启用延迟;没有的话派发保持原样(立即执行,无内容)。
|
|
264
|
+
- 注入文本只认 `source.kind === 'user'` 的直接用户消息;系统注入(agent/plugin 来源)不会完成派发。
|
|
265
|
+
- 回合内没有直接用户消息(如目标续跑回合)时,`turn/start` 在 `turn/end` 时**不带内容**派发;新回合开始会先冲掉上一个未认领的 `turn/start`。
|
|
266
|
+
- 直接用户回合中,延迟通常只有几毫秒(`user/message` 紧随 `turn/start`),先于任何步骤/工具事件。
|
|
267
|
+
|
|
171
268
|
## 执行历史
|
|
172
269
|
|
|
173
270
|
每次 hook 触发都会记入内存环形缓冲(默认 500 条),并 best-effort 追加到 `~/.dsh/dsh-hooks/history.jsonl`(权限 0600)——供未来 UI 与调试使用。环形缓冲在启动时从 JSONL 回填,且 Web 面板每次读取时增量同步磁盘上新增的记录(包括其他 dsh 进程的追加,如任务看板 Host),因此重启后历史不会消失。记录不含 secret(环境变量从不入记录):
|
|
@@ -183,7 +280,7 @@ config:
|
|
|
183
280
|
hooks: […]
|
|
184
281
|
```
|
|
185
282
|
|
|
186
|
-
每条记录:时间戳、kind(run/notify)、事件、命令、会话、结果(spawned / exit-0 / exit-nonzero / timeout / sent / send-failed…)、退出码、耗时、stderr 尾部。写盘失败静默吞掉,绝不阻塞 hook。
|
|
283
|
+
每条记录:时间戳、kind(run/notify)、事件、命令、会话、结果(spawned / exit-0 / exit-nonzero / timeout / skipped / sent / send-failed…)、退出码、耗时、stderr 尾部。写盘失败静默吞掉,绝不阻塞 hook。
|
|
187
284
|
|
|
188
285
|
## dry-run:验证配置
|
|
189
286
|
|
package/lib/client.js
CHANGED
|
@@ -200,18 +200,22 @@ window.__ModuleLoader__.load({
|
|
|
200
200
|
const EVENTS = [
|
|
201
201
|
"turn/start",
|
|
202
202
|
"turn/end",
|
|
203
|
+
"tree/settled",
|
|
203
204
|
"step/end",
|
|
204
205
|
"tool/call",
|
|
205
206
|
"tool/result",
|
|
206
207
|
"user/message",
|
|
207
208
|
"approval/asked",
|
|
209
|
+
"approval/decided",
|
|
208
210
|
"session/title",
|
|
209
211
|
"session/created",
|
|
210
212
|
"session/disposed",
|
|
211
213
|
"agent/created",
|
|
212
214
|
"agent/disposed",
|
|
213
215
|
"agent/error",
|
|
214
|
-
"agent/status"
|
|
216
|
+
"agent/status",
|
|
217
|
+
"hook/failed",
|
|
218
|
+
"usage/daily"
|
|
215
219
|
];
|
|
216
220
|
const TURN_END_REASONS = [
|
|
217
221
|
"completed",
|
|
@@ -252,6 +256,10 @@ window.__ModuleLoader__.load({
|
|
|
252
256
|
if (hook.timeoutMs !== void 0 && hook.timeoutMs !== 1e4) lines.push(` timeoutMs: ${hook.timeoutMs}`);
|
|
253
257
|
if (hook.retries !== void 0 && hook.retries !== 0) lines.push(` retries: ${hook.retries}`);
|
|
254
258
|
if (hook.retryDelayMs !== void 0 && hook.retryDelayMs !== 500) lines.push(` retryDelayMs: ${hook.retryDelayMs}`);
|
|
259
|
+
if (hook.enabled === false) lines.push(" enabled: false");
|
|
260
|
+
if (hook.cwd !== void 0 && hook.cwd !== "") lines.push(` cwd: ${q(hook.cwd)}`);
|
|
261
|
+
if (hook.maxConcurrent !== void 0) lines.push(` maxConcurrent: ${hook.maxConcurrent}`);
|
|
262
|
+
if (hook.debounceMs !== void 0) lines.push(` debounceMs: ${hook.debounceMs}`);
|
|
255
263
|
return lines.join("\n");
|
|
256
264
|
}
|
|
257
265
|
async function copyText(text) {
|
|
@@ -285,7 +293,11 @@ window.__ModuleLoader__.load({
|
|
|
285
293
|
notify: draft.notify ?? null,
|
|
286
294
|
timeoutMs: draft.timeoutMs,
|
|
287
295
|
retries: draft.retries,
|
|
288
|
-
retryDelayMs: draft.retryDelayMs
|
|
296
|
+
retryDelayMs: draft.retryDelayMs,
|
|
297
|
+
enabled: draft.enabled === false ? false : void 0,
|
|
298
|
+
cwd: draft.cwd === "" ? void 0 : draft.cwd,
|
|
299
|
+
maxConcurrent: draft.maxConcurrent,
|
|
300
|
+
debounceMs: draft.debounceMs
|
|
289
301
|
};
|
|
290
302
|
}
|
|
291
303
|
const NEW_HOOK = {
|
|
@@ -483,7 +495,11 @@ window.__ModuleLoader__.load({
|
|
|
483
495
|
},
|
|
484
496
|
timeoutMs: hook.timeoutMs,
|
|
485
497
|
retries: hook.retries,
|
|
486
|
-
retryDelayMs: hook.retryDelayMs
|
|
498
|
+
retryDelayMs: hook.retryDelayMs,
|
|
499
|
+
enabled: hook.enabled,
|
|
500
|
+
cwd: hook.cwd,
|
|
501
|
+
maxConcurrent: hook.maxConcurrent,
|
|
502
|
+
debounceMs: hook.debounceMs
|
|
487
503
|
})));
|
|
488
504
|
setSaveMessage(null);
|
|
489
505
|
setEditing(true);
|
|
@@ -1122,7 +1138,7 @@ window.__ModuleLoader__.load({
|
|
|
1122
1138
|
className: "dh-input",
|
|
1123
1139
|
value: pattern,
|
|
1124
1140
|
onChange: (e) => patchMatchValue(index, field, e.target.value),
|
|
1125
|
-
placeholder: "
|
|
1141
|
+
placeholder: "正则或数值比较(^(rm|git|ssh) / >10000)"
|
|
1126
1142
|
}),
|
|
1127
1143
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1128
1144
|
type: "button",
|
|
@@ -1187,6 +1203,67 @@ window.__ModuleLoader__.load({
|
|
|
1187
1203
|
children: "删除"
|
|
1188
1204
|
})
|
|
1189
1205
|
]
|
|
1206
|
+
}),
|
|
1207
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1208
|
+
className: "dh-test-row",
|
|
1209
|
+
children: [
|
|
1210
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1211
|
+
className: "dh-field",
|
|
1212
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1213
|
+
className: "dh-field-label",
|
|
1214
|
+
children: "enabled"
|
|
1215
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1216
|
+
className: "dh-input",
|
|
1217
|
+
value: hook.enabled === false ? "false" : "true",
|
|
1218
|
+
onChange: (e) => patchDraft(index, { enabled: e.target.value === "true" }),
|
|
1219
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1220
|
+
value: "true",
|
|
1221
|
+
children: "启用"
|
|
1222
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1223
|
+
value: "false",
|
|
1224
|
+
children: "停用"
|
|
1225
|
+
})]
|
|
1226
|
+
})]
|
|
1227
|
+
}),
|
|
1228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1229
|
+
className: "dh-field",
|
|
1230
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1231
|
+
className: "dh-field-label",
|
|
1232
|
+
children: "cwd"
|
|
1233
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1234
|
+
className: "dh-input",
|
|
1235
|
+
value: hook.cwd ?? "",
|
|
1236
|
+
onChange: (e) => patchDraft(index, { cwd: e.target.value }),
|
|
1237
|
+
placeholder: "session 或绝对路径"
|
|
1238
|
+
})]
|
|
1239
|
+
}),
|
|
1240
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1241
|
+
className: "dh-field",
|
|
1242
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1243
|
+
className: "dh-field-label",
|
|
1244
|
+
children: "maxConcurrent"
|
|
1245
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1246
|
+
className: "dh-input",
|
|
1247
|
+
type: "number",
|
|
1248
|
+
value: hook.maxConcurrent ?? "",
|
|
1249
|
+
onChange: (e) => patchDraft(index, { maxConcurrent: parseNum(e.target.value) }),
|
|
1250
|
+
placeholder: "不限"
|
|
1251
|
+
})]
|
|
1252
|
+
}),
|
|
1253
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1254
|
+
className: "dh-field",
|
|
1255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1256
|
+
className: "dh-field-label",
|
|
1257
|
+
children: "debounceMs"
|
|
1258
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1259
|
+
className: "dh-input",
|
|
1260
|
+
type: "number",
|
|
1261
|
+
value: hook.debounceMs ?? "",
|
|
1262
|
+
onChange: (e) => patchDraft(index, { debounceMs: parseNum(e.target.value) }),
|
|
1263
|
+
placeholder: "0"
|
|
1264
|
+
})]
|
|
1265
|
+
})
|
|
1266
|
+
]
|
|
1190
1267
|
})
|
|
1191
1268
|
]
|
|
1192
1269
|
}, index)),
|
|
@@ -1244,7 +1321,7 @@ window.__ModuleLoader__.load({
|
|
|
1244
1321
|
}),
|
|
1245
1322
|
hook.match !== void 0 && Object.keys(hook.match).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1246
1323
|
className: "dh-hook-match",
|
|
1247
|
-
children: Object.entries(hook.match).map(([field, pattern]) => `${field} =~ /${pattern}/`).join(",")
|
|
1324
|
+
children: Object.entries(hook.match).map(([field, pattern]) => /^([<>]=?|=)\s*-?\d/.test(pattern) ? `${field} ${pattern}` : `${field} =~ /${pattern}/`).join(",")
|
|
1248
1325
|
}),
|
|
1249
1326
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1250
1327
|
className: "dh-hook-meta",
|
|
@@ -1256,10 +1333,14 @@ window.__ModuleLoader__.load({
|
|
|
1256
1333
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1257
1334
|
className: "dh-feishu-hint",
|
|
1258
1335
|
children: [
|
|
1336
|
+
hook.enabled === false ? "已停用 · " : "",
|
|
1259
1337
|
"timeout ",
|
|
1260
1338
|
hook.timeoutMs ?? 1e4,
|
|
1261
1339
|
"ms · retries ",
|
|
1262
|
-
hook.retries ?? 0
|
|
1340
|
+
hook.retries ?? 0,
|
|
1341
|
+
hook.cwd !== void 0 && hook.cwd !== "" ? ` · cwd: ${hook.cwd}` : "",
|
|
1342
|
+
hook.maxConcurrent !== void 0 ? ` · 并发≤${hook.maxConcurrent}` : "",
|
|
1343
|
+
hook.debounceMs !== void 0 && hook.debounceMs > 0 ? ` · 去抖 ${hook.debounceMs}ms` : ""
|
|
1263
1344
|
]
|
|
1264
1345
|
})]
|
|
1265
1346
|
})
|
package/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Hookable event kinds. v1 is emit-only: no waterfall/interception events. */
|
|
2
|
-
export declare const HOOK_EVENTS: readonly ['turn/start', 'turn/end', 'tree/settled', 'step/end', 'tool/call', 'tool/result', 'user/message', 'approval/asked', 'approval/decided', 'session/title', 'session/created', 'session/disposed', 'agent/created', 'agent/disposed', 'agent/error', 'agent/status', 'hook/failed'];
|
|
2
|
+
export declare const HOOK_EVENTS: readonly ['turn/start', 'turn/end', 'tree/settled', 'step/end', 'tool/call', 'tool/result', 'user/message', 'approval/asked', 'approval/decided', 'session/title', 'session/created', 'session/disposed', 'agent/created', 'agent/disposed', 'agent/error', 'agent/status', 'hook/failed', 'usage/daily'];
|
|
3
3
|
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
4
4
|
/** `turn/end` reason kinds (from @deepseek-ai/dsh-session TurnEndReasonMap). */
|
|
5
5
|
export declare const TURN_END_REASONS: readonly ['completed', 'error', 'aborted', 'blocked', 'max-tokens', 'interrupted'];
|
|
@@ -17,7 +17,20 @@ export interface NotifySpec {
|
|
|
17
17
|
/** webhook: post a Slack-style `{ text }` one-line summary instead of the full context document. */
|
|
18
18
|
slack?: boolean;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Numeric comparison filter for `match`: every declared op must hold for
|
|
22
|
+
* the (numeric) context field. Declared in YAML as an object (`{ gt: 10000 }`)
|
|
23
|
+
* or as an equivalent string (`'>10000'`); both compare against the field's
|
|
24
|
+
* number value, never its string form.
|
|
25
|
+
*/
|
|
26
|
+
export interface NumericMatch {
|
|
27
|
+
gt?: number;
|
|
28
|
+
gte?: number;
|
|
29
|
+
lt?: number;
|
|
30
|
+
lte?: number;
|
|
31
|
+
eq?: number;
|
|
32
|
+
}
|
|
33
|
+
/** One hook: a matching event runs `run` (or sends `notify`). */
|
|
21
34
|
export interface HookSpec {
|
|
22
35
|
/** Event that triggers the hook. */
|
|
23
36
|
on: HookEvent;
|
|
@@ -27,12 +40,15 @@ export interface HookSpec {
|
|
|
27
40
|
*/
|
|
28
41
|
when?: TurnEndReasonKind;
|
|
29
42
|
/**
|
|
30
|
-
* Optional field →
|
|
31
|
-
* context's field value for the hook to run.
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
43
|
+
* Optional field → filter map: every declared filter must match the
|
|
44
|
+
* context's field value for the hook to run. Values are regexes (tested
|
|
45
|
+
* against the String-coerced field) or numeric comparisons (`{ gt: 10000 }`
|
|
46
|
+
* / `'>10000'`, numbers only). Fields are `HookContext` keys (`tool`,
|
|
47
|
+
* `sessionName`, `sessionId`, `error`, `source`, `cwd`, `content`, `turn`,
|
|
48
|
+
* `durationMs`, `runningSubagents`, …); a field absent from the context
|
|
49
|
+
* never matches.
|
|
34
50
|
*/
|
|
35
|
-
match?: Record<string, RegExp>;
|
|
51
|
+
match?: Record<string, RegExp | NumericMatch>;
|
|
36
52
|
/**
|
|
37
53
|
* Command to spawn through the platform shell. Exactly one of `run` and
|
|
38
54
|
* `notify` must be declared.
|
|
@@ -55,6 +71,29 @@ export interface HookSpec {
|
|
|
55
71
|
retries?: number;
|
|
56
72
|
/** Base delay between retries in milliseconds; doubles per attempt. Defaults to 500. */
|
|
57
73
|
retryDelayMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Disable this hook without deleting it: the declaration stays in config,
|
|
76
|
+
* dispatch skips it silently (never counted as a failure). Defaults to true.
|
|
77
|
+
*/
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Working directory for the spawned command. `'session'` runs in the
|
|
81
|
+
* session's cwd (the project the agent works on); any other value must be
|
|
82
|
+
* an absolute path. Defaults to the plugin process directory.
|
|
83
|
+
*/
|
|
84
|
+
cwd?: 'session' | string;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of concurrently running processes for this hook.
|
|
87
|
+
* Triggers beyond the limit are dropped (recorded as `skipped`). Defaults
|
|
88
|
+
* to unlimited; `0` also means unlimited.
|
|
89
|
+
*/
|
|
90
|
+
maxConcurrent?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Debounce window in milliseconds for high-frequency events (step/end,
|
|
93
|
+
* tool/*, …): triggers inside the window collapse into one trailing
|
|
94
|
+
* execution carrying the latest context. Defaults to 0 (disabled).
|
|
95
|
+
*/
|
|
96
|
+
debounceMs?: number;
|
|
58
97
|
}
|
|
59
98
|
/** Execution-history settings: in-memory ring buffer + optional JSONL log. */
|
|
60
99
|
export interface HistoryConfig {
|