dsh-hooks 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -4
- package/README.zh.md +31 -4
- package/bin/dsh-hooks.mjs +110 -10
- package/lib/client.js +281 -55
- package/lib/config.d.ts +4 -2
- package/lib/config.js +1 -1
- package/lib/dry-run.d.ts +29 -0
- package/lib/dry-run.js +93 -13
- package/lib/events.js +32 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +7 -2
- package/lib/notify.d.ts +32 -6
- package/lib/notify.js +55 -19
- package/lib/server.js +21 -4
- package/lib/tail.d.ts +44 -0
- package/lib/tail.js +154 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,7 +113,11 @@ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `
|
|
|
113
113
|
|
|
114
114
|
## Command execution
|
|
115
115
|
|
|
116
|
-
- Each matching hook spawns `run` through the platform shell, **fire-and-forget**: failures only `console.warn`, never retried by default
|
|
116
|
+
- Each matching hook spawns `run` through the platform shell, **fire-and-forget**: failures only `console.warn`, never retried by default, never block the agent loop. Command stdout/stderr is captured (64 KiB per stream); on a non-zero exit the stderr tail is appended to the warning log.
|
|
117
|
+
- **Retries** (`retries` / `retryDelayMs`) apply to both execution channels with the same shape: up to `retries` extra attempts after the first one, with the delay doubling per attempt (`retryDelayMs`, default 500 ms):
|
|
118
|
+
- `run`: retries **non-zero exit codes** only (spawn failures and timeouts are never retried).
|
|
119
|
+
- `notify` webhook channel: retries **transport failures** (connection reset, timeout) and HTTP **408 / 429 / 5xx**; other 4xx mean the request itself is wrong and are not retried. The default `retries: 0` now means exactly one attempt — before 0.13 the webhook channel hard-coded a single transport retry, which is now folded into `retries`: existing configs that relied on it should set `retries: 1`.
|
|
120
|
+
- `notify` desktop channel is a local popup and never retries.
|
|
117
121
|
- Context is passed via **environment variables** (no shell injection through data):
|
|
118
122
|
|
|
119
123
|
| Variable | Meaning |
|
|
@@ -299,6 +303,16 @@ Every hook trigger is recorded into an in-memory ring buffer (default 500 entrie
|
|
|
299
303
|
|
|
300
304
|
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.
|
|
301
305
|
|
|
306
|
+
To follow the log while you work, use `tail` (Ctrl+C to quit):
|
|
307
|
+
|
|
308
|
+
```sh
|
|
309
|
+
dsh-hooks tail # replay the last 10, then follow
|
|
310
|
+
dsh-hooks tail --event turn/end --outcome exit-nonzero # only failed turn ends
|
|
311
|
+
dsh-hooks tail --hook notify-feishu --n 50 --json # 50 backfill lines, raw JSONL for jq
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`tail` resolves the JSONL path from the profile's `history.path`, falling back to the default without failing when the config file is missing or mid-edit. It reads only appended bytes, waits for complete lines (a record observed mid-write stays pending), and restarts from byte 0 when the file is truncated or rotated.
|
|
315
|
+
|
|
302
316
|
## dry-run: verify config
|
|
303
317
|
|
|
304
318
|
Simulate an event to see which hooks would fire and why the others are filtered:
|
|
@@ -313,6 +327,17 @@ dsh-hooks dry-run turn/end --reason completed --profile web
|
|
|
313
327
|
dsh-hooks dry-run tool/call --tool ssh_exec --execute # end-to-end: actually run the matching hooks
|
|
314
328
|
```
|
|
315
329
|
|
|
330
|
+
**Simulating numeric fields**: give count/timing/token fields a value to exercise numeric `match` filters:
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
dsh-hooks dry-run turn/end --running-subagents 3
|
|
334
|
+
dsh-hooks dry-run turn/end --duration-ms 1250 --usage-input 120000 --usage-output 45000
|
|
335
|
+
dsh-hooks dry-run tool/result --tool-duration-ms 15000
|
|
336
|
+
dsh-hooks dry-run usage/daily --field usageCacheReadTokens=90000 # generic: --field <name>=<value>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Simulatable fields: `turn`, `step`, `durationMs`, `toolDurationMs`, `runningSubagents`, `totalSubagents`, `treeDurationMs`, `usageTurns`, `usageSessions`, `usageInputTokens`, `usageOutputTokens`, `usageCacheReadTokens`, `usageCacheWriteTokens`, `usageReasoningTokens`. Anything outside the list (or a non-finite number) is reported as "ignored" rather than dropped silently. The mock mirrors the runtime: `turn/end` always carries `runningSubagents` (0 by default), so the documented `match: { runningSubagents: '^0$' }` pattern is reachable in dry-run too; `usage/daily` carries "yesterday" plus non-zero token details.
|
|
340
|
+
|
|
316
341
|
`dry-run` reads the profile's `cordis.patch.yml` (the `id: dsh-hooks` block) and validates the config (bad regexes fail here).
|
|
317
342
|
|
|
318
343
|
## Web GUI
|
|
@@ -320,11 +345,11 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # end-to-end: actually r
|
|
|
320
345
|
After install, the dsh web settings panel gains a "Hooks" section (beside General and Plugins):
|
|
321
346
|
|
|
322
347
|
- **Status badges**: plugin version, hook count, history count, plus live diagnostics (in-flight runs, recent failures)
|
|
323
|
-
- **Manual tester**: pick an event (
|
|
348
|
+
- **Manual tester**: pick an event (18 kinds) + reason/tool, and fill the "simulated fields" row with numeric context (`runningSubagents`, `durationMs`, usage in/out); "Simulate" shows the per-hook match report, "Execute" really triggers the matching hooks; the report clears when the inputs change
|
|
324
349
|
- **Notify-channel tests**: fire a test notification at the webhook (optional Slack summary) / desktop channel and show the payload preview
|
|
325
350
|
- **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)
|
|
326
351
|
- **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
|
|
327
|
-
- **Execution-history timeline**: at the bottom of the card, **collapsed by default** (the toggle state persists in localStorage
|
|
352
|
+
- **Execution-history timeline**: at the bottom of the card, **collapsed by default** (the toggle state persists in localStorage), refreshed every 5s. Expanded, it filters by **event / outcome / session** (filters persist in localStorage, with a "showing N of M" count and a clear button) and **exports the current view as JSONL** (same shape as the on-disk `history.jsonl`, file name stamped with local time); the panel fetches the latest 200 records and filters in the browser
|
|
328
353
|
|
|
329
354
|
CLI/headless environments are unaffected: the browser half loads only in the web GUI and the core has no UI runtime dependencies.
|
|
330
355
|
|
|
@@ -336,7 +361,7 @@ In the web profile (when the shared webServer service exists) dsh-hooks register
|
|
|
336
361
|
| --- | --- | --- |
|
|
337
362
|
| `/dsh-hooks/status` | GET | plugin version, hook count, history count, the **current hook list**, and live runner stats |
|
|
338
363
|
| `/dsh-hooks/history?n=50` | GET | the latest N execution records (JSON envelope) |
|
|
339
|
-
| `/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 |
|
|
364
|
+
| `/dsh-hooks/test` | POST | simulate an event: `{"event":"tool/call","tool":"ssh_exec","execute":false}` returns a per-hook match report; `fields` overrides numeric context (`{"event":"turn/end","fields":{"runningSubagents":2}}`, unknown fields → 400); `execute: true` actually runs the matching hooks |
|
|
340
365
|
| `/dsh-hooks/notify/test` | POST | fire a test notification at a channel: `{"channel":"webhook","url":…,"slack":true}` or `{"channel":"desktop"}`; returns the payload preview |
|
|
341
366
|
| `/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 |
|
|
342
367
|
| `/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 |
|
package/README.zh.md
CHANGED
|
@@ -113,7 +113,11 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
113
113
|
|
|
114
114
|
## 命令执行
|
|
115
115
|
|
|
116
|
-
- 每个命中的 hook 通过系统 shell 执行 `run`,**fire-and-forget**:失败只 `console.warn
|
|
116
|
+
- 每个命中的 hook 通过系统 shell 执行 `run`,**fire-and-forget**:失败只 `console.warn`、默认不重试、绝不阻塞 agent 循环。命令的 stdout/stderr 会被捕获(各 64 KiB 上限),非零退出码时把 stderr 尾部写进告警日志。
|
|
117
|
+
- **重试**(`retries` / `retryDelayMs`)对两个执行通道都生效,都是「首次尝试之后再重试 N 次」、间隔按 `retryDelayMs` 逐次翻倍(默认 500ms):
|
|
118
|
+
- `run`:只重试**非零退出码**(spawn 失败与超时不重试)。
|
|
119
|
+
- `notify` 的 webhook 渠道:重试**传输失败**(连接被重置、超时)与 HTTP **408 / 429 / 5xx**;其余 4xx 表示请求本身有问题,不重试。**默认 `retries: 0` 表示只尝试一次**——0.13 之前 webhook 曾硬编码「传输失败自动再试一次」,现在这层兜底已并入 `retries`,需要它的老配置请显式写 `retries: 1`。
|
|
120
|
+
- `notify` 的 desktop 渠道是本地弹窗,不重试。
|
|
117
121
|
- 上下文通过**环境变量**传递(数据不拼接进 shell 字符串,防注入):
|
|
118
122
|
|
|
119
123
|
| 变量 | 含义 |
|
|
@@ -282,6 +286,16 @@ config:
|
|
|
282
286
|
|
|
283
287
|
每条记录:时间戳、kind(run/notify)、事件、命令、会话、结果(spawned / exit-0 / exit-nonzero / timeout / skipped / sent / send-failed…)、退出码、耗时、stderr 尾部。写盘失败静默吞掉,绝不阻塞 hook。
|
|
284
288
|
|
|
289
|
+
终端里想边跑边看,用 `tail`(Ctrl+C 退出):
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
dsh-hooks tail # 回放最近 10 条,然后实时跟进
|
|
293
|
+
dsh-hooks tail --event turn/end --outcome exit-nonzero # 只看失败的回合结束
|
|
294
|
+
dsh-hooks tail --hook notify-feishu --n 50 --json # 50 条起,输出原始 JSONL 供 jq
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`tail` 的 JSONL 路径取自 profile 配置的 `history.path`(未配置或配置读不出来时回落到默认路径,不会因为配置文件半途改动而报错)。它只读新增字节、容忍半行(等换行再输出)、文件被截断/轮转时自动从 0 重新跟进。
|
|
298
|
+
|
|
285
299
|
## dry-run:验证配置
|
|
286
300
|
|
|
287
301
|
配置完先用 `dry-run` 模拟事件,看哪些 hook 会触发、哪些被过滤:
|
|
@@ -296,6 +310,19 @@ dsh-hooks dry-run turn/end --reason completed --profile web
|
|
|
296
310
|
dsh-hooks dry-run tool/call --tool ssh_exec --execute # 端到端真跑匹配的 hook
|
|
297
311
|
```
|
|
298
312
|
|
|
313
|
+
**模拟数值字段**:数字类上下文(`runningSubagents`、`durationMs`、`toolDurationMs`、`usage*`…)可以直接给定值,用来验证基于数值的 `match`:
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
dsh-hooks dry-run turn/end --running-subagents 3
|
|
317
|
+
dsh-hooks dry-run turn/end --duration-ms 1250 --usage-input 120000 --usage-output 45000
|
|
318
|
+
dsh-hooks dry-run tool/result --tool-duration-ms 15000
|
|
319
|
+
dsh-hooks dry-run usage/daily --field usageCacheReadTokens=90000 # 通用写法:--field <名>=<值>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
可模拟字段白名单:`turn`、`step`、`durationMs`、`toolDurationMs`、`runningSubagents`、`totalSubagents`、`treeDurationMs`、`usageTurns`、`usageSessions`、`usageInputTokens`、`usageOutputTokens`、`usageCacheReadTokens`、`usageCacheWriteTokens`、`usageReasoningTokens`。非白名单字段或非法数字不会被静默丢弃——报告里会列出「已忽略无法模拟的字段」。
|
|
323
|
+
|
|
324
|
+
模拟上下文与运行时保持一致:`turn/end` 一定带 `runningSubagents`(默认 0),所以 README 推荐的 `match: { runningSubagents: '^0$' }` 在 dry-run 里也能命中;`usage/daily` 带「昨天」与非零 token 明细。
|
|
325
|
+
|
|
299
326
|
`dry-run` 直接读 profile 的 `cordis.patch.yml`(`id: dsh-hooks` 配置块),配置校验(非法正则等)会在这一步报错。
|
|
300
327
|
|
|
301
328
|
## Web GUI
|
|
@@ -303,11 +330,11 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # 端到端真跑匹配
|
|
|
303
330
|
安装后,dsh web 的设置面板里会出现「Hooks」分区(与「通用」「插件」平级):
|
|
304
331
|
|
|
305
332
|
- **状态徽章**:插件版本、hook 数、历史条数,以及运行诊断(正在执行的 hook 数、最近失败数)
|
|
306
|
-
- **手动测试**:选事件(
|
|
333
|
+
- **手动测试**:选事件(18 类)+ reason/tool,并可用「模拟字段」一行填入 `runningSubagents` / `durationMs` / usage 输入输出等数值上下文;「模拟」看逐 hook 匹配报告,「执行」真实触发;切换输入自动清空旧结果
|
|
307
334
|
- **通知渠道测试**:向 webhook(可选 Slack 摘要)/ desktop 渠道发一条测试通知,显示发送内容预览
|
|
308
335
|
- **飞书通知**:网页内扫码连接飞书——显示二维码(含有效期倒计时、可取消),扫码后自动创建应用、写入凭据与 hook 配置;已连接后显示应用摘要,可一键发送测试卡片、调整卡片截断长度(50–5000 字符,默认 300,带正文预览)、重新扫码换绑或断开连接(可选一并移除飞书 hooks)
|
|
309
336
|
- **当前 hooks**:只读清单(事件/when/match/run/notify + 超时重试参数),一键「复制 YAML」;点「编辑」进入表单编辑器,增删改 hook 后写回 `cordis.patch.yml`(自动备份原文件、写前校验正则与 run/notify 二选一,保存即热加载)
|
|
310
|
-
- **执行历史时间线**:位于分区底部、**默认折叠**(展开状态记忆于 localStorage
|
|
337
|
+
- **执行历史时间线**:位于分区底部、**默认折叠**(展开状态记忆于 localStorage),5 秒自动刷新。展开后可按**事件 / 结果 / 会话**过滤(条件同样记忆在 localStorage,附「显示 N / 共 M 条」计数与「清空过滤」),并把当前视图**导出为 JSONL**(与磁盘上的 `history.jsonl` 同格式,文件名带本地时间戳)——面板一次拉取最近 200 条,过滤在浏览器侧完成
|
|
311
338
|
|
|
312
339
|
CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心零 UI 运行时依赖。
|
|
313
340
|
|
|
@@ -319,7 +346,7 @@ web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 `/ds
|
|
|
319
346
|
| --- | --- | --- |
|
|
320
347
|
| `/dsh-hooks/status` | GET | 插件版本、hook 数、历史条数、**当前 hooks 清单**与运行统计 |
|
|
321
348
|
| `/dsh-hooks/history?n=50` | GET | 最近 N 条执行历史(JSON envelope) |
|
|
322
|
-
| `/dsh-hooks/test` | POST | 模拟事件评估:`{"event":"tool/call","tool":"ssh_exec","execute":false}` 返回逐 hook
|
|
349
|
+
| `/dsh-hooks/test` | POST | 模拟事件评估:`{"event":"tool/call","tool":"ssh_exec","execute":false}` 返回逐 hook 匹配报告;可用 `fields` 覆盖数值上下文(`{"event":"turn/end","fields":{"runningSubagents":2}}`,非法字段返回 400);`execute: true` 真跑匹配的 hook |
|
|
323
350
|
| `/dsh-hooks/notify/test` | POST | 向指定渠道发测试通知:`{"channel":"webhook","url":…,"slack":true}` 或 `{"channel":"desktop"}`,返回发送内容预览 |
|
|
324
351
|
| `/dsh-hooks/hooks/save` | POST | 保存 hook 列表:`{"profile":"web","hooks":[…]}`——校验(事件/when/正则/run-notify 二选一)后写回 cordis.patch.yml,自动备份原文件 |
|
|
325
352
|
| `/dsh-hooks/feishu/status` | GET | 飞书连接摘要(app id / 目标均已打码,绝不返回 secret)+ 扫码会话快照 + 截断长度 + 正文预览 |
|
package/bin/dsh-hooks.mjs
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { spawn } from 'node:child_process'
|
|
27
27
|
import QRCode from 'qrcode'
|
|
28
|
-
import { runDryRun } from '../lib/dry-run.js'
|
|
28
|
+
import { loadHistoryPath, runDryRun } from '../lib/dry-run.js'
|
|
29
|
+
import { formatTailRecord, HistoryTailer, matchesTailFilter } from '../lib/tail.js'
|
|
29
30
|
import {
|
|
30
31
|
FEISHU_CONFIG_PATH,
|
|
31
32
|
mergePatchYaml,
|
|
@@ -120,18 +121,99 @@ function cliArgs(args) {
|
|
|
120
121
|
|
|
121
122
|
/** Parse the dry-run flags; the first positional arg is the event. */
|
|
122
123
|
function cliDryRunArgs(args) {
|
|
123
|
-
const opts = { event: '', execute: false }
|
|
124
|
+
const opts = { event: '', execute: false, fields: {} }
|
|
125
|
+
/** Numeric context fields the CLI exposes by name (plus the generic --field). */
|
|
126
|
+
const fieldFlags = {
|
|
127
|
+
'--running-subagents': 'runningSubagents',
|
|
128
|
+
'--duration-ms': 'durationMs',
|
|
129
|
+
'--tool-duration-ms': 'toolDurationMs',
|
|
130
|
+
'--usage-input': 'usageInputTokens',
|
|
131
|
+
'--usage-output': 'usageOutputTokens',
|
|
132
|
+
}
|
|
133
|
+
for (let i = 0; i < args.length; i++) {
|
|
134
|
+
const flag = args[i]
|
|
135
|
+
if (flag === '--reason') opts.reason = args[++i]
|
|
136
|
+
else if (flag === '--tool') opts.tool = args[++i]
|
|
137
|
+
else if (flag === '--session-name') opts.sessionName = args[++i]
|
|
138
|
+
else if (flag === '--profile') opts.profile = args[++i]
|
|
139
|
+
else if (flag === '--execute') opts.execute = true
|
|
140
|
+
else if (fieldFlags[flag] !== undefined) opts.fields[fieldFlags[flag]] = cliNumber(args[++i])
|
|
141
|
+
else if (flag === '--field') {
|
|
142
|
+
// Generic escape hatch: --field usageCacheReadTokens=90000
|
|
143
|
+
const [name, raw] = String(args[++i] ?? '').split('=')
|
|
144
|
+
if (name) opts.fields[name] = cliNumber(raw)
|
|
145
|
+
} else if (!flag.startsWith('-') && opts.event === '') opts.event = flag
|
|
146
|
+
}
|
|
147
|
+
return opts
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* CLI numbers stay numbers when they parse (the mock only accepts finite
|
|
152
|
+
* numbers); anything else is passed through so the dry-run report can name
|
|
153
|
+
* the field as ignored instead of silently dropping it.
|
|
154
|
+
*/
|
|
155
|
+
function cliNumber(raw) {
|
|
156
|
+
const value = Number(raw)
|
|
157
|
+
return raw !== undefined && raw !== '' && Number.isFinite(value) ? value : raw
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Parse the tail flags. */
|
|
161
|
+
function cliTailArgs(args) {
|
|
162
|
+
const opts = { json: false }
|
|
124
163
|
for (let i = 0; i < args.length; i++) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
else if (
|
|
128
|
-
else if (
|
|
129
|
-
else if (
|
|
130
|
-
else if (
|
|
164
|
+
const flag = args[i]
|
|
165
|
+
if (flag === '--profile') opts.profile = args[++i]
|
|
166
|
+
else if (flag === '--n') opts.n = cliNumber(args[++i])
|
|
167
|
+
else if (flag === '--event') opts.event = args[++i]
|
|
168
|
+
else if (flag === '--outcome') opts.outcome = args[++i]
|
|
169
|
+
else if (flag === '--hook') opts.hook = args[++i]
|
|
170
|
+
else if (flag === '--interval') opts.intervalMs = cliNumber(args[++i])
|
|
171
|
+
else if (flag === '--file') opts.file = args[++i]
|
|
172
|
+
else if (flag === '--json') opts.json = true
|
|
131
173
|
}
|
|
132
174
|
return opts
|
|
133
175
|
}
|
|
134
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Follow the history JSONL: print the last `n` records, then everything
|
|
179
|
+
* appended afterwards. Returns a stop function (the CLI wires it to SIGINT,
|
|
180
|
+
* tests call it directly).
|
|
181
|
+
*/
|
|
182
|
+
export async function tailHistory(options = {}) {
|
|
183
|
+
const print = options.print ?? console.log
|
|
184
|
+
const intervalMs = typeof options.intervalMs === 'number' && options.intervalMs > 0 ? options.intervalMs : 500
|
|
185
|
+
const file = options.file ?? loadHistoryPath(options.profile ?? 'web')
|
|
186
|
+
const filter = { event: options.event, outcome: options.outcome, hook: options.hook }
|
|
187
|
+
const active = Object.entries(filter).filter(([, value]) => value !== undefined)
|
|
188
|
+
const tailer = new HistoryTailer(file)
|
|
189
|
+
|
|
190
|
+
const emit = (records) => {
|
|
191
|
+
for (const record of records) {
|
|
192
|
+
if (!matchesTailFilter(record, filter)) continue
|
|
193
|
+
print(options.json ? JSON.stringify(record) : formatTailRecord(record))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
print(`dsh-hooks tail · ${file}`)
|
|
198
|
+
if (active.length > 0) print(`过滤:${active.map(([key, value]) => `${key}=${value}`).join(' ')}`)
|
|
199
|
+
const backfill = tailer.backfill(typeof options.n === 'number' ? options.n : 10)
|
|
200
|
+
if (backfill.length === 0) print('(暂无历史记录)')
|
|
201
|
+
emit(backfill)
|
|
202
|
+
print('—— 实时跟进中(Ctrl+C 退出)——')
|
|
203
|
+
|
|
204
|
+
const timer = setInterval(() => {
|
|
205
|
+
try {
|
|
206
|
+
const batch = tailer.readNew()
|
|
207
|
+
if (batch.reset) print(`⚠ 文件被截断或轮转,已从头跟进:${file}`)
|
|
208
|
+
emit(batch.records)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
print(`⚠ 读取失败:${error instanceof Error ? error.message : String(error)}`)
|
|
211
|
+
}
|
|
212
|
+
}, intervalMs)
|
|
213
|
+
|
|
214
|
+
return () => clearInterval(timer)
|
|
215
|
+
}
|
|
216
|
+
|
|
135
217
|
function isDirectRun() {
|
|
136
218
|
try {
|
|
137
219
|
return process.argv[1] !== undefined && import.meta.url === new URL(`file:///${process.argv[1].replace(/\\/g, '/')}`).href
|
|
@@ -159,7 +241,7 @@ function runCli() {
|
|
|
159
241
|
} else if (command === 'dry-run') {
|
|
160
242
|
const opts = cliDryRunArgs(args)
|
|
161
243
|
if (!opts.event) {
|
|
162
|
-
console.error('缺少事件参数,用法:dsh-hooks dry-run <event> [--reason <kind>] [--profile <name>] [--execute]')
|
|
244
|
+
console.error('缺少事件参数,用法:dsh-hooks dry-run <event> [--reason <kind>] [--profile <name>] [--execute] [--running-subagents N]')
|
|
163
245
|
process.exit(1)
|
|
164
246
|
}
|
|
165
247
|
runDryRun(opts)
|
|
@@ -168,12 +250,30 @@ function runCli() {
|
|
|
168
250
|
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`)
|
|
169
251
|
process.exit(1)
|
|
170
252
|
})
|
|
253
|
+
} else if (command === 'tail') {
|
|
254
|
+
const opts = cliTailArgs(args)
|
|
255
|
+
tailHistory(opts)
|
|
256
|
+
.then((stop) => {
|
|
257
|
+
process.on('SIGINT', () => {
|
|
258
|
+
stop()
|
|
259
|
+
process.exit(0)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
.catch((error) => {
|
|
263
|
+
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`)
|
|
264
|
+
process.exit(1)
|
|
265
|
+
})
|
|
171
266
|
} else {
|
|
172
267
|
console.error(`用法:
|
|
173
268
|
dsh-hooks feishu-setup [--profile <name>] 扫码创建飞书通知机器人并自动配置
|
|
174
269
|
dsh-hooks feishu-test 验证配置并发送测试卡片
|
|
175
270
|
dsh-hooks dry-run <event> [--reason <kind>] [--tool <name>] [--profile <name>] [--execute]
|
|
176
|
-
|
|
271
|
+
[--running-subagents N] [--duration-ms N] [--tool-duration-ms N]
|
|
272
|
+
[--usage-input N] [--usage-output N] [--field <名>=<值>]
|
|
273
|
+
模拟事件,列出会触发/被过滤的 hook(--execute 实际执行)
|
|
274
|
+
dsh-hooks tail [--profile <name>] [--n <count>] [--event <name>] [--outcome <name>] [--hook <text>]
|
|
275
|
+
[--json] [--interval <ms>] [--file <path>]
|
|
276
|
+
实时跟踪执行历史(history.jsonl),Ctrl+C 退出`)
|
|
177
277
|
process.exit(command === '--help' || command === 'help' || command === undefined ? 0 : 1)
|
|
178
278
|
}
|
|
179
279
|
}
|