dsh-hooks 0.11.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 +29 -5
- package/README.zh.md +29 -5
- package/lib/client.js +5 -1
- package/lib/config.d.ts +1 -1
- package/lib/config.js +2 -1
- package/lib/context.d.ts +10 -1
- package/lib/context.js +6 -0
- package/lib/dry-run.js +16 -2
- package/lib/events.d.ts +12 -8
- package/lib/events.js +28 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +28 -2
- package/lib/server.d.ts +1 -1
- package/lib/usage.d.ts +79 -0
- package/lib/usage.js +110 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ Every hook field:
|
|
|
107
107
|
| `agent/error` | The agent loop reports an error | error text |
|
|
108
108
|
| `agent/status` | Agent status transition | status |
|
|
109
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 |
|
|
110
111
|
|
|
111
112
|
The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `error`, …). Hooks for other events run unconditionally.
|
|
112
113
|
|
|
@@ -134,11 +135,11 @@ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `
|
|
|
134
135
|
| `DSH_HOOK_STATUS` | agent status (`agent/status`) |
|
|
135
136
|
| `DSH_HOOK_ERROR` | error text (`agent/error`, and the failure message on `turn/end` error) |
|
|
136
137
|
| `DSH_HOOK_CONTENT` | event content snapshot: turn assistant text, tool result text, user message text, turn-initiating message text (turn/start) |
|
|
137
|
-
| `DSH_HOOK_USAGE_INPUT_TOKENS` |
|
|
138
|
-
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` |
|
|
139
|
-
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` |
|
|
140
|
-
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` |
|
|
141
|
-
| `DSH_HOOK_USAGE_REASONING_TOKENS` |
|
|
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) |
|
|
142
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") |
|
|
143
144
|
| `DSH_HOOK_PARENT_SESSION_ID` | parent session id (subagent lineage; absent for top-level sessions) |
|
|
144
145
|
| `DSH_HOOK_SUBAGENT` | `1` when the session is a subagent child, `0` otherwise |
|
|
@@ -151,6 +152,9 @@ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `
|
|
|
151
152
|
| `DSH_HOOK_TREE_DURATION_MS` | parent turn/end → tree settle duration, ms (`tree/settled`) |
|
|
152
153
|
| `DSH_HOOK_FAILED_HOOK` | identity summary of the hook that failed consecutively (`hook/failed`) |
|
|
153
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`) |
|
|
154
158
|
| `DSH_HOOK_TIMESTAMP` | ISO timestamp |
|
|
155
159
|
|
|
156
160
|
- `{{var}}` placeholders inside `run` are substituted from the same context, e.g. `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`.
|
|
@@ -184,6 +188,26 @@ For the simpler "notify only once the whole tree settles" pattern, the synthetic
|
|
|
184
188
|
|
|
185
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).
|
|
186
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
|
+
|
|
187
211
|
### Numeric match comparisons
|
|
188
212
|
|
|
189
213
|
Numeric context fields (`turn`, `step`, `durationMs`, `toolDurationMs`, `usage*`, `runningSubagents`, …) support real comparisons instead of regex hacks:
|
package/README.zh.md
CHANGED
|
@@ -107,6 +107,7 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
107
107
|
| `agent/error` | Agent 循环报错 | 错误文本 |
|
|
108
108
|
| `agent/status` | Agent 状态切换 | 状态 |
|
|
109
109
|
| `hook/failed` | 同一 hook 连续失败达到 `failedAlertThreshold`(默认 3;合成事件,从结果流发射) | 失败 hook 摘要、连续失败次数 |
|
|
110
|
+
| `usage/daily` | 本地日历日翻篇后的下一个事件(合成事件,无定时器):报告刚结束那一天的 token 用量 | 覆盖日期、当日回合数、贡献会话数、当日 token 明细 |
|
|
110
111
|
|
|
111
112
|
`turn/end` 的 `when` 匹配结束原因(`completed`、`error`…);其他事件的 hook 无条件执行。
|
|
112
113
|
|
|
@@ -134,11 +135,11 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
134
135
|
| `DSH_HOOK_STATUS` | Agent 状态(agent/status) |
|
|
135
136
|
| `DSH_HOOK_ERROR` | 错误文本(agent/error,以及 turn/end 出错时的失败详情) |
|
|
136
137
|
| `DSH_HOOK_CONTENT` | 事件内容快照:回合最后助手文本、工具结果文本、用户消息文本、回合触发消息文本(turn/start) |
|
|
137
|
-
| `DSH_HOOK_USAGE_INPUT_TOKENS` |
|
|
138
|
-
| `DSH_HOOK_USAGE_OUTPUT_TOKENS` |
|
|
139
|
-
| `DSH_HOOK_USAGE_CACHE_READ_TOKENS` |
|
|
140
|
-
| `DSH_HOOK_USAGE_CACHE_WRITE_TOKENS` |
|
|
141
|
-
| `DSH_HOOK_USAGE_REASONING_TOKENS` |
|
|
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(有上报时,同上) |
|
|
142
143
|
| `DSH_HOOK_RUNNING_SUBAGENTS` | 本会话下仍在运行的存活子代理数(turn/end;`0` = 无——让 hook 能区分「工作已交给后台子代理」与「回合真正结束」) |
|
|
143
144
|
| `DSH_HOOK_PARENT_SESSION_ID` | 父会话 id(子代理谱系;顶层会话无此变量) |
|
|
144
145
|
| `DSH_HOOK_SUBAGENT` | 会话为子代理时为 `1`,否则 `0` |
|
|
@@ -151,6 +152,9 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
|
151
152
|
| `DSH_HOOK_TREE_DURATION_MS` | 父回合结束 → 树落定的耗时(毫秒,`tree/settled`) |
|
|
152
153
|
| `DSH_HOOK_FAILED_HOOK` | 连续失败的 hook 身份摘要(`hook/failed`) |
|
|
153
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`) |
|
|
154
158
|
| `DSH_HOOK_TIMESTAMP` | ISO 时间戳 |
|
|
155
159
|
|
|
156
160
|
- `run` 里的 `{{变量}}` 占位符会从同一上下文替换,例如 `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`。
|
|
@@ -185,6 +189,26 @@ config:
|
|
|
185
189
|
|
|
186
190
|
已落定但闲置(idle)的 continuable 子代理不计入运行中,不会一直压住通知。落定监视是事件驱动且 best-effort 的:插件重启后监视集合丢失;重查失败会静默放弃该监视(不会补发迟到的通知)。
|
|
187
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
|
+
|
|
188
212
|
### match 数值比较
|
|
189
213
|
|
|
190
214
|
对数字字段(`turn`、`step`、`durationMs`、`toolDurationMs`、`usage*`、`runningSubagents`…)可以直接写数值比较,不用绕正则:
|
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",
|
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'];
|
package/lib/config.js
CHANGED
|
@@ -18,6 +18,7 @@ export const HOOK_EVENTS = [
|
|
|
18
18
|
'agent/error',
|
|
19
19
|
'agent/status',
|
|
20
20
|
'hook/failed',
|
|
21
|
+
'usage/daily',
|
|
21
22
|
];
|
|
22
23
|
/** `turn/end` reason kinds (from @deepseek-ai/dsh-session TurnEndReasonMap). */
|
|
23
24
|
export const TURN_END_REASONS = [
|
|
@@ -34,7 +35,7 @@ export const TURN_END_REASONS = [
|
|
|
34
35
|
// declaration self-contained.
|
|
35
36
|
export const Config = Schema.object({
|
|
36
37
|
hooks: Schema.array(Schema.object({
|
|
37
|
-
on: Schema.union([...HOOK_EVENTS]).description('触发事件: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'),
|
|
38
|
+
on: Schema.union([...HOOK_EVENTS]).description('触发事件: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'),
|
|
38
39
|
when: Schema.union([...TURN_END_REASONS]).description('可选过滤:对 turn/end 匹配结束原因(completed/error/aborted/blocked/max-tokens/interrupted);其他事件忽略该字段'),
|
|
39
40
|
match: Schema.dict(Schema.union([
|
|
40
41
|
Schema.regExp(),
|
package/lib/context.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface HookContext {
|
|
|
35
35
|
* pairing tool/call was never seen, e.g. after a plugin restart).
|
|
36
36
|
*/
|
|
37
37
|
toolDurationMs?: number;
|
|
38
|
-
/** Aggregated token usage of the turn (turn/end),
|
|
38
|
+
/** Aggregated token usage of the turn (turn/end), or of the day (usage/daily). */
|
|
39
39
|
usageInputTokens?: number;
|
|
40
40
|
usageOutputTokens?: number;
|
|
41
41
|
usageCacheReadTokens?: number;
|
|
@@ -69,6 +69,15 @@ export interface HookContext {
|
|
|
69
69
|
hookFailedHook?: string;
|
|
70
70
|
/** Consecutive failure count when the alert fired (hook/failed). */
|
|
71
71
|
hookFailures?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Local calendar day (`YYYY-MM-DD`) the token totals below cover
|
|
74
|
+
* (usage/daily only; turn/end carries a single turn, not a day).
|
|
75
|
+
*/
|
|
76
|
+
usageDay?: string;
|
|
77
|
+
/** Turns with reported accounting that day (usage/daily). */
|
|
78
|
+
usageTurns?: number;
|
|
79
|
+
/** Distinct sessions that contributed usage that day (usage/daily). */
|
|
80
|
+
usageSessions?: number;
|
|
72
81
|
timestamp: string;
|
|
73
82
|
}
|
|
74
83
|
export declare function toEnv(ctx: HookContext): Record<string, string>;
|
package/lib/context.js
CHANGED
|
@@ -71,6 +71,12 @@ export function toEnv(ctx) {
|
|
|
71
71
|
env.DSH_HOOK_FAILED_HOOK = ctx.hookFailedHook;
|
|
72
72
|
if (ctx.hookFailures !== undefined)
|
|
73
73
|
env.DSH_HOOK_FAILURES = String(ctx.hookFailures);
|
|
74
|
+
if (ctx.usageDay !== undefined)
|
|
75
|
+
env.DSH_HOOK_USAGE_DAY = ctx.usageDay;
|
|
76
|
+
if (ctx.usageTurns !== undefined)
|
|
77
|
+
env.DSH_HOOK_USAGE_TURNS = String(ctx.usageTurns);
|
|
78
|
+
if (ctx.usageSessions !== undefined)
|
|
79
|
+
env.DSH_HOOK_USAGE_SESSIONS = String(ctx.usageSessions);
|
|
74
80
|
return env;
|
|
75
81
|
}
|
|
76
82
|
/** Render `{{DSH_HOOK_*}}` placeholders from the context map. */
|
package/lib/dry-run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import YAML from 'yaml';
|
|
10
10
|
import { Config } from './config.js';
|
|
11
11
|
import { matchFilters } from './events.js';
|
|
12
|
+
import { localDayKey } from './usage.js';
|
|
12
13
|
import { createHookRunner } from './runner.js';
|
|
13
14
|
import { fireNotify } from './notify.js';
|
|
14
15
|
/** Profile patch file for a profile name. */
|
|
@@ -48,7 +49,7 @@ export function loadHooks(profile, paths = {}) {
|
|
|
48
49
|
}
|
|
49
50
|
/** A synthetic context for the simulated event, overridable per field. */
|
|
50
51
|
export function mockContext(event, overrides = {}) {
|
|
51
|
-
|
|
52
|
+
const ctx = {
|
|
52
53
|
event,
|
|
53
54
|
sessionId: 'dry-run',
|
|
54
55
|
sessionName: 'dry-run 会话',
|
|
@@ -59,8 +60,21 @@ export function mockContext(event, overrides = {}) {
|
|
|
59
60
|
callId: 'dry-run-call',
|
|
60
61
|
content: 'dry-run 模拟内容',
|
|
61
62
|
timestamp: new Date().toISOString(),
|
|
62
|
-
...overrides,
|
|
63
63
|
};
|
|
64
|
+
if (event === 'usage/daily') {
|
|
65
|
+
// A daily report always describes a day that already ended, and the
|
|
66
|
+
// simulated numbers must be non-zero so `match` filters on them (e.g.
|
|
67
|
+
// `{ usageInputTokens: '>0' }`) are actually exercisable.
|
|
68
|
+
ctx.usageDay = localDayKey(new Date(Date.now() - 86_400_000));
|
|
69
|
+
ctx.usageTurns = 12;
|
|
70
|
+
ctx.usageSessions = 3;
|
|
71
|
+
ctx.usageInputTokens = 120_000;
|
|
72
|
+
ctx.usageOutputTokens = 45_000;
|
|
73
|
+
ctx.usageCacheReadTokens = 90_000;
|
|
74
|
+
ctx.usageCacheWriteTokens = 6_000;
|
|
75
|
+
ctx.usageReasoningTokens = 8_000;
|
|
76
|
+
}
|
|
77
|
+
return { ...ctx, ...overrides };
|
|
64
78
|
}
|
|
65
79
|
/** Render a match value (regex source, comparison op, or object form). */
|
|
66
80
|
function matchText(value) {
|
package/lib/events.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Session, SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session';
|
|
2
2
|
import type { HookContext } from './context.js';
|
|
3
3
|
import type { HookSpec, NumericMatch, TurnEndReasonKind } from './config.js';
|
|
4
|
+
import type { DailyUsageTotals, UsageTotals } from './usage.js';
|
|
4
5
|
import type { AgentLike } from './types.js';
|
|
6
|
+
export type { UsageTotals } from './usage.js';
|
|
5
7
|
/** `approval/asked` payload (merge-extensible, declared by dsh-user-approval). */
|
|
6
8
|
export interface ApprovalAskedData {
|
|
7
9
|
id: string;
|
|
@@ -65,14 +67,6 @@ export declare function sessionTitle(session: Session): string | undefined;
|
|
|
65
67
|
* their own display truncation.
|
|
66
68
|
*/
|
|
67
69
|
export declare function turnContent(session: Session, turn: number): string | undefined;
|
|
68
|
-
/** Aggregated turn usage for hook contexts (only fields actually reported). */
|
|
69
|
-
export interface UsageTotals {
|
|
70
|
-
inputTokens: number;
|
|
71
|
-
outputTokens: number;
|
|
72
|
-
cacheReadTokens?: number;
|
|
73
|
-
cacheWriteTokens?: number;
|
|
74
|
-
reasoningTokens?: number;
|
|
75
|
-
}
|
|
76
70
|
/**
|
|
77
71
|
* Sum the `usage` of every `assistant/message` of a turn. Steps without
|
|
78
72
|
* reported accounting are skipped; returns undefined when no step reported
|
|
@@ -128,6 +122,16 @@ export declare function treeSettledContext(session: Session, totalSubagents: num
|
|
|
128
122
|
* session identity of the event that triggered the failing hook.
|
|
129
123
|
*/
|
|
130
124
|
export declare function hookFailedContext(origin: HookContext, hookFailedHook: string, hookFailures: number): HookContext;
|
|
125
|
+
/**
|
|
126
|
+
* Synthetic `usage/daily` context: the local calendar day that just ended,
|
|
127
|
+
* with its aggregated token usage. Emitted by index.ts when the day rolls
|
|
128
|
+
* over (detected from ordinary event traffic — no timers); `origin` supplies
|
|
129
|
+
* the session identity of the event that triggered the report.
|
|
130
|
+
*
|
|
131
|
+
* The token fields reuse the `turn/end` names on purpose: a hook reads the
|
|
132
|
+
* same variables, with the day's aggregate instead of one turn's.
|
|
133
|
+
*/
|
|
134
|
+
export declare function usageDailyContext(origin: HookContext, totals: DailyUsageTotals): HookContext;
|
|
131
135
|
export declare function agentCreatedContext(agent: AgentLike): HookContext;
|
|
132
136
|
export declare function agentDisposedContext(agent: AgentLike): HookContext;
|
|
133
137
|
export declare function agentErrorContext(agent: AgentLike, turn: number | undefined, error: unknown): HookContext;
|
package/lib/events.js
CHANGED
|
@@ -403,6 +403,34 @@ export function hookFailedContext(origin, hookFailedHook, hookFailures) {
|
|
|
403
403
|
timestamp: new Date().toISOString(),
|
|
404
404
|
};
|
|
405
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Synthetic `usage/daily` context: the local calendar day that just ended,
|
|
408
|
+
* with its aggregated token usage. Emitted by index.ts when the day rolls
|
|
409
|
+
* over (detected from ordinary event traffic — no timers); `origin` supplies
|
|
410
|
+
* the session identity of the event that triggered the report.
|
|
411
|
+
*
|
|
412
|
+
* The token fields reuse the `turn/end` names on purpose: a hook reads the
|
|
413
|
+
* same variables, with the day's aggregate instead of one turn's.
|
|
414
|
+
*/
|
|
415
|
+
export function usageDailyContext(origin, totals) {
|
|
416
|
+
return {
|
|
417
|
+
event: 'usage/daily',
|
|
418
|
+
sessionId: origin.sessionId,
|
|
419
|
+
sessionName: origin.sessionName,
|
|
420
|
+
cwd: origin.cwd,
|
|
421
|
+
usageDay: totals.day,
|
|
422
|
+
usageTurns: totals.turns,
|
|
423
|
+
usageSessions: totals.sessions,
|
|
424
|
+
usageInputTokens: totals.inputTokens,
|
|
425
|
+
usageOutputTokens: totals.outputTokens,
|
|
426
|
+
// Only fields some turn actually reported: an absent variable is easier to
|
|
427
|
+
// reason about (and to match on) than one that is present-but-undefined.
|
|
428
|
+
...(totals.cacheReadTokens !== undefined ? { usageCacheReadTokens: totals.cacheReadTokens } : {}),
|
|
429
|
+
...(totals.cacheWriteTokens !== undefined ? { usageCacheWriteTokens: totals.cacheWriteTokens } : {}),
|
|
430
|
+
...(totals.reasoningTokens !== undefined ? { usageReasoningTokens: totals.reasoningTokens } : {}),
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
406
434
|
export function agentCreatedContext(agent) {
|
|
407
435
|
return {
|
|
408
436
|
event: 'agent/created',
|
package/lib/index.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export { createHistorySink } from './history.js';
|
|
|
59
59
|
* Model-facing announcement, installed only when the system-prompt service
|
|
60
60
|
* exists (web profile). Tells agents the plugin exists and how to cooperate.
|
|
61
61
|
*/
|
|
62
|
-
export declare const DSH_HOOKS_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hooks \u63D2\u4EF6\uFF08DeepSeek Harness \u914D\u7F6E\u9A71\u52A8\u751F\u547D\u5468\u671F hooks\uFF09\uFF1A\u53EF\u5728 profile \u7684 cordis.patch.yml \u58F0\u660E\u300C\u4E8B\u4EF6 \u2192 \u547D\u4EE4/\u901A\u77E5\u300D\u7684 hook\uFF08turn/start\u3001turn/end\u3001tree/settled\u3001step/end\u3001tool/call\u3001tool/result\u3001user/message\u3001approval/asked\u3001approval/decided\u3001session/title\u3001session/created\u3001session/disposed\u3001agent/created\u3001agent/disposed\u3001agent/error\u3001agent/status\u3001hook/failed \u5171
|
|
62
|
+
export declare const DSH_HOOKS_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hooks \u63D2\u4EF6\uFF08DeepSeek Harness \u914D\u7F6E\u9A71\u52A8\u751F\u547D\u5468\u671F hooks\uFF09\uFF1A\u53EF\u5728 profile \u7684 cordis.patch.yml \u58F0\u660E\u300C\u4E8B\u4EF6 \u2192 \u547D\u4EE4/\u901A\u77E5\u300D\u7684 hook\uFF08turn/start\u3001turn/end\u3001tree/settled\u3001step/end\u3001tool/call\u3001tool/result\u3001user/message\u3001approval/asked\u3001approval/decided\u3001session/title\u3001session/created\u3001session/disposed\u3001agent/created\u3001agent/disposed\u3001agent/error\u3001agent/status\u3001hook/failed\u3001usage/daily \u5171 18 \u7C7B\u4E8B\u4EF6\uFF09\uFF0C\u652F\u6301 when \u539F\u56E0\u8FC7\u6EE4\u3001match \u5B57\u6BB5\u6B63\u5219/\u6570\u503C\u6BD4\u8F83\u8FC7\u6EE4\uFF08\u5982 '>10000'\uFF09\u3001stdin JSON \u8F93\u5165\u3001opt-in \u91CD\u8BD5\u3001\u6267\u884C\u9009\u9879\uFF08enabled \u505C\u7528 / cwd \u5DE5\u4F5C\u76EE\u5F55 / maxConcurrent + debounceMs \u9632\u9AD8\u9891\u98CE\u66B4\uFF09\u3001\u5185\u7F6E webhook/desktop \u901A\u77E5\u6E20\u9053\uFF1B\u6267\u884C\u5386\u53F2\u8BB0\u5F55\u4E8E ~/.dsh/dsh-hooks/history.jsonl\uFF1B`dsh-hooks dry-run <event>` \u53EF\u6A21\u62DF\u4E8B\u4EF6\u9A8C\u8BC1\u914D\u7F6E\u3002\u7528\u6237\u63D0\u5230\u300Chooks / \u94A9\u5B50 / \u751F\u547D\u5468\u671F / \u901A\u77E5\u914D\u7F6E\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
63
63
|
export declare function apply(ctx: Context, config?: Config): void;
|
|
64
64
|
export declare const _internals: {
|
|
65
65
|
clearTurnTracking: typeof clearTurnTracking;
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import './types.js';
|
|
2
2
|
import { Config } from './config.js';
|
|
3
|
-
import { agentCreatedContext, agentDisposedContext, agentErrorContext, agentStatusContext, classifySessionEvent, clearTurnTracking, hookFailedContext, hookMatches, matchFilters, sessionCreatedContext, sessionDisposedContext, treeSettledContext, } from './events.js';
|
|
3
|
+
import { agentCreatedContext, agentDisposedContext, agentErrorContext, agentStatusContext, classifySessionEvent, clearTurnTracking, hookFailedContext, hookMatches, matchFilters, sessionCreatedContext, sessionDisposedContext, treeSettledContext, usageDailyContext, } from './events.js';
|
|
4
4
|
import { eventLabel } from './context.js';
|
|
5
|
+
import { DailyUsageAccumulator, usageTotalsFromContext } from './usage.js';
|
|
5
6
|
import { createHookRunner } from './runner.js';
|
|
6
7
|
import { fireNotify } from './notify.js';
|
|
7
8
|
import { createHistorySink } from './history.js';
|
|
@@ -73,7 +74,7 @@ export { createHistorySink } from './history.js';
|
|
|
73
74
|
* Model-facing announcement, installed only when the system-prompt service
|
|
74
75
|
* exists (web profile). Tells agents the plugin exists and how to cooperate.
|
|
75
76
|
*/
|
|
76
|
-
export const DSH_HOOKS_GUIDANCE = '本机已安装 dsh-hooks 插件(DeepSeek Harness 配置驱动生命周期 hooks):可在 profile 的 cordis.patch.yml 声明「事件 → 命令/通知」的 hook(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 共
|
|
77
|
+
export const DSH_HOOKS_GUIDANCE = '本机已安装 dsh-hooks 插件(DeepSeek Harness 配置驱动生命周期 hooks):可在 profile 的 cordis.patch.yml 声明「事件 → 命令/通知」的 hook(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 共 18 类事件),支持 when 原因过滤、match 字段正则/数值比较过滤(如 \'>10000\')、stdin JSON 输入、opt-in 重试、执行选项(enabled 停用 / cwd 工作目录 / maxConcurrent + debounceMs 防高频风暴)、内置 webhook/desktop 通知渠道;执行历史记录于 ~/.dsh/dsh-hooks/history.jsonl;`dsh-hooks dry-run <event>` 可模拟事件验证配置。用户提到「hooks / 钩子 / 生命周期 / 通知配置」时即指本插件,请据此协作。';
|
|
77
78
|
export function apply(ctx, config = {}) {
|
|
78
79
|
const hooks = config.hooks ?? [];
|
|
79
80
|
const history = createHistorySink(config.history ?? undefined);
|
|
@@ -184,6 +185,25 @@ export function apply(ctx, config = {}) {
|
|
|
184
185
|
};
|
|
185
186
|
// Per-hook debounce state (trailing timers); cleared on dispose.
|
|
186
187
|
const debounceTimers = new Map();
|
|
188
|
+
// Synthetic usage/daily: an in-memory per-day token bucket. Bookkeeping is
|
|
189
|
+
// wired only when a usage/daily hook exists — with none declared, no
|
|
190
|
+
// accumulation and no day check happen at all. The day rollover is detected
|
|
191
|
+
// from ordinary event traffic (no timers): the first classified event of a
|
|
192
|
+
// new day reports the day that just ended. Token totals include every
|
|
193
|
+
// session, subagent turns included — they are billed to the same account.
|
|
194
|
+
const hasUsageDailyHooks = hooks.some((hook) => hook.on === 'usage/daily' && hook.enabled !== false);
|
|
195
|
+
const usageDays = new DailyUsageAccumulator();
|
|
196
|
+
/**
|
|
197
|
+
* Feed one classified event to the daily bucket and dispatch the finished
|
|
198
|
+
* day's report when the calendar day rolled over. `origin` supplies the
|
|
199
|
+
* session identity of the triggering event.
|
|
200
|
+
*/
|
|
201
|
+
const trackUsageDay = (origin, sessionId) => {
|
|
202
|
+
const totals = origin.event === 'turn/end' ? usageTotalsFromContext(origin) : undefined;
|
|
203
|
+
const finished = usageDays.observe(totals === undefined ? undefined : { totals, sessionId });
|
|
204
|
+
if (finished !== undefined)
|
|
205
|
+
runMatching(usageDailyContext(origin, finished.totals));
|
|
206
|
+
};
|
|
187
207
|
// turn/start content: the session log records `turn/start` BEFORE the
|
|
188
208
|
// turn's `user/message`, so the initiating prompt text cannot be read at
|
|
189
209
|
// turn-start time. When turn/start hooks exist, dispatch is deferred until
|
|
@@ -281,6 +301,11 @@ export function apply(ctx, config = {}) {
|
|
|
281
301
|
return;
|
|
282
302
|
const reasonKind = extractReasonKind(event);
|
|
283
303
|
const sessionId = String(session.id);
|
|
304
|
+
// Day-rollover check runs before any dispatch, so a turn ending just after
|
|
305
|
+
// midnight is reported against the previous day and then bucketed into the
|
|
306
|
+
// new one.
|
|
307
|
+
if (hasUsageDailyHooks)
|
|
308
|
+
trackUsageDay(classified, sessionId);
|
|
284
309
|
if (classified.event === 'turn/start') {
|
|
285
310
|
if (!hasTurnStartHooks) {
|
|
286
311
|
runMatching(classified, reasonKind);
|
|
@@ -362,6 +387,7 @@ export function apply(ctx, config = {}) {
|
|
|
362
387
|
debounceTimers.clear();
|
|
363
388
|
pendingTurnStarts.clear();
|
|
364
389
|
watchedTrees.clear();
|
|
390
|
+
usageDays.reset();
|
|
365
391
|
});
|
|
366
392
|
}
|
|
367
393
|
/** Extract the `turn/end` reason kind from a session event, when present. */
|
package/lib/server.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface HookRoutesOptions {
|
|
|
48
48
|
/** Sanitized per-hook description for the settings panel (regex sources, no RegExp objects). */
|
|
49
49
|
export declare function describeHooks(hooks: readonly HookSpec[]): {
|
|
50
50
|
index: number;
|
|
51
|
-
on: "agent/created" | "agent/disposed" | "agent/error" | "agent/status" | "approval/asked" | "approval/decided" | "hook/failed" | "session/created" | "session/disposed" | "session/title" | "step/end" | "tool/call" | "tool/result" | "tree/settled" | "turn/end" | "turn/start" | "user/message";
|
|
51
|
+
on: "agent/created" | "agent/disposed" | "agent/error" | "agent/status" | "approval/asked" | "approval/decided" | "hook/failed" | "session/created" | "session/disposed" | "session/title" | "step/end" | "tool/call" | "tool/result" | "tree/settled" | "turn/end" | "turn/start" | "usage/daily" | "user/message";
|
|
52
52
|
when: "aborted" | "blocked" | "completed" | "error" | "interrupted" | "max-tokens" | undefined;
|
|
53
53
|
match: {
|
|
54
54
|
[k: string]: string;
|
package/lib/usage.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily token accounting behind the synthetic `usage/daily` event.
|
|
3
|
+
*
|
|
4
|
+
* The contract is deliberately modest: accumulate in memory, detect the local
|
|
5
|
+
* calendar-day rollover from ordinary event traffic (no timers, no scheduled
|
|
6
|
+
* tasks), and report the day that just ended. Two consequences are documented
|
|
7
|
+
* in the READMEs: a plugin-process restart drops the in-flight day, and a day
|
|
8
|
+
* followed by no further events is reported at the next event rather than at
|
|
9
|
+
* midnight.
|
|
10
|
+
*/
|
|
11
|
+
import type { HookContext } from './context.js';
|
|
12
|
+
/** Structural token accounting (disjoint counts; cache fields optional). */
|
|
13
|
+
export interface UsageTotals {
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
cacheReadTokens?: number;
|
|
17
|
+
cacheWriteTokens?: number;
|
|
18
|
+
reasoningTokens?: number;
|
|
19
|
+
}
|
|
20
|
+
/** One finished day's aggregate, as carried by `usage/daily`. */
|
|
21
|
+
export interface DailyUsageTotals extends UsageTotals {
|
|
22
|
+
/** Local calendar day the totals cover (`YYYY-MM-DD`). */
|
|
23
|
+
day: string;
|
|
24
|
+
/** Turns that reported accounting and contributed to the totals. */
|
|
25
|
+
turns: number;
|
|
26
|
+
/** Distinct sessions that contributed usage that day. */
|
|
27
|
+
sessions: number;
|
|
28
|
+
}
|
|
29
|
+
/** A finished day handed to the caller when the calendar day rolled over. */
|
|
30
|
+
export interface DailyUsageRollover {
|
|
31
|
+
day: string;
|
|
32
|
+
totals: DailyUsageTotals;
|
|
33
|
+
}
|
|
34
|
+
/** One observation fed to the accumulator (a `turn/end` that reported usage). */
|
|
35
|
+
export interface UsageObservation {
|
|
36
|
+
totals: UsageTotals;
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Local calendar day key (`YYYY-MM-DD`). Local — not UTC — because a daily
|
|
41
|
+
* report should follow the machine's day boundary the way the user reads
|
|
42
|
+
* costs; `toISOString` would put the boundary in the wrong place.
|
|
43
|
+
*/
|
|
44
|
+
export declare function localDayKey(date?: Date): string;
|
|
45
|
+
/**
|
|
46
|
+
* Read the turn usage already flattened onto a hook context (the same numbers
|
|
47
|
+
* a `turn/end` hook sees, so a `usage/daily` report and the per-turn variables
|
|
48
|
+
* always agree). Returns undefined when the turn reported no accounting.
|
|
49
|
+
*/
|
|
50
|
+
export declare function usageTotalsFromContext(ctx: HookContext): UsageTotals | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* In-memory daily usage bucket behind the synthetic `usage/daily` event.
|
|
53
|
+
*
|
|
54
|
+
* `observe` is the single entry point: it rolls the calendar day over first
|
|
55
|
+
* (returning the finished day's report exactly once), then records the
|
|
56
|
+
* observation into the new day. Rolling over *before* recording is what keeps
|
|
57
|
+
* a turn ending just after midnight out of the previous day's totals.
|
|
58
|
+
*/
|
|
59
|
+
export declare class DailyUsageAccumulator {
|
|
60
|
+
#private;
|
|
61
|
+
/** The day currently accumulated; `undefined` before the first observation. */
|
|
62
|
+
get day(): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Roll the day over if needed, then record one observation.
|
|
65
|
+
*
|
|
66
|
+
* Returns the finished day's totals when this call crossed a day boundary
|
|
67
|
+
* and that day had reported usage — `undefined` on an ordinary call, on the
|
|
68
|
+
* first observation of a process (nothing accumulated yet), and for a day
|
|
69
|
+
* without usage (an empty report is noise, not a report).
|
|
70
|
+
*/
|
|
71
|
+
observe(observation?: UsageObservation, now?: Date): DailyUsageRollover | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Detect a calendar-day rollover without recording anything, so the first
|
|
74
|
+
* event after midnight can report the day that just ended.
|
|
75
|
+
*/
|
|
76
|
+
rollover(now?: Date): DailyUsageRollover | undefined;
|
|
77
|
+
/** Drop all state (plugin dispose). */
|
|
78
|
+
reset(): void;
|
|
79
|
+
}
|
package/lib/usage.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local calendar day key (`YYYY-MM-DD`). Local — not UTC — because a daily
|
|
3
|
+
* report should follow the machine's day boundary the way the user reads
|
|
4
|
+
* costs; `toISOString` would put the boundary in the wrong place.
|
|
5
|
+
*/
|
|
6
|
+
export function localDayKey(date = new Date()) {
|
|
7
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
9
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the turn usage already flattened onto a hook context (the same numbers
|
|
13
|
+
* a `turn/end` hook sees, so a `usage/daily` report and the per-turn variables
|
|
14
|
+
* always agree). Returns undefined when the turn reported no accounting.
|
|
15
|
+
*/
|
|
16
|
+
export function usageTotalsFromContext(ctx) {
|
|
17
|
+
if (ctx.usageInputTokens === undefined && ctx.usageOutputTokens === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
return {
|
|
20
|
+
inputTokens: ctx.usageInputTokens ?? 0,
|
|
21
|
+
outputTokens: ctx.usageOutputTokens ?? 0,
|
|
22
|
+
...(ctx.usageCacheReadTokens !== undefined ? { cacheReadTokens: ctx.usageCacheReadTokens } : {}),
|
|
23
|
+
...(ctx.usageCacheWriteTokens !== undefined ? { cacheWriteTokens: ctx.usageCacheWriteTokens } : {}),
|
|
24
|
+
...(ctx.usageReasoningTokens !== undefined ? { reasoningTokens: ctx.usageReasoningTokens } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* In-memory daily usage bucket behind the synthetic `usage/daily` event.
|
|
29
|
+
*
|
|
30
|
+
* `observe` is the single entry point: it rolls the calendar day over first
|
|
31
|
+
* (returning the finished day's report exactly once), then records the
|
|
32
|
+
* observation into the new day. Rolling over *before* recording is what keeps
|
|
33
|
+
* a turn ending just after midnight out of the previous day's totals.
|
|
34
|
+
*/
|
|
35
|
+
export class DailyUsageAccumulator {
|
|
36
|
+
#day;
|
|
37
|
+
#bucket;
|
|
38
|
+
/** The day currently accumulated; `undefined` before the first observation. */
|
|
39
|
+
get day() {
|
|
40
|
+
return this.#day;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Roll the day over if needed, then record one observation.
|
|
44
|
+
*
|
|
45
|
+
* Returns the finished day's totals when this call crossed a day boundary
|
|
46
|
+
* and that day had reported usage — `undefined` on an ordinary call, on the
|
|
47
|
+
* first observation of a process (nothing accumulated yet), and for a day
|
|
48
|
+
* without usage (an empty report is noise, not a report).
|
|
49
|
+
*/
|
|
50
|
+
observe(observation, now = new Date()) {
|
|
51
|
+
const finished = this.rollover(now);
|
|
52
|
+
if (observation !== undefined)
|
|
53
|
+
this.#record(observation);
|
|
54
|
+
return finished;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Detect a calendar-day rollover without recording anything, so the first
|
|
58
|
+
* event after midnight can report the day that just ended.
|
|
59
|
+
*/
|
|
60
|
+
rollover(now = new Date()) {
|
|
61
|
+
const today = localDayKey(now);
|
|
62
|
+
if (this.#day === undefined) {
|
|
63
|
+
this.#day = today;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (this.#day === today)
|
|
67
|
+
return undefined;
|
|
68
|
+
const bucket = this.#bucket;
|
|
69
|
+
const day = this.#day;
|
|
70
|
+
this.#day = today;
|
|
71
|
+
this.#bucket = undefined;
|
|
72
|
+
if (bucket === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
day,
|
|
76
|
+
totals: {
|
|
77
|
+
day,
|
|
78
|
+
inputTokens: bucket.inputTokens,
|
|
79
|
+
outputTokens: bucket.outputTokens,
|
|
80
|
+
...(bucket.cacheReadTokens !== undefined ? { cacheReadTokens: bucket.cacheReadTokens } : {}),
|
|
81
|
+
...(bucket.cacheWriteTokens !== undefined ? { cacheWriteTokens: bucket.cacheWriteTokens } : {}),
|
|
82
|
+
...(bucket.reasoningTokens !== undefined ? { reasoningTokens: bucket.reasoningTokens } : {}),
|
|
83
|
+
turns: bucket.turns,
|
|
84
|
+
sessions: bucket.sessions.size,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Drop all state (plugin dispose). */
|
|
89
|
+
reset() {
|
|
90
|
+
this.#day = undefined;
|
|
91
|
+
this.#bucket = undefined;
|
|
92
|
+
}
|
|
93
|
+
#record(observation) {
|
|
94
|
+
const bucket = (this.#bucket ??= { inputTokens: 0, outputTokens: 0, turns: 0, sessions: new Set() });
|
|
95
|
+
bucket.inputTokens += observation.totals.inputTokens;
|
|
96
|
+
bucket.outputTokens += observation.totals.outputTokens;
|
|
97
|
+
if (observation.totals.cacheReadTokens !== undefined) {
|
|
98
|
+
bucket.cacheReadTokens = (bucket.cacheReadTokens ?? 0) + observation.totals.cacheReadTokens;
|
|
99
|
+
}
|
|
100
|
+
if (observation.totals.cacheWriteTokens !== undefined) {
|
|
101
|
+
bucket.cacheWriteTokens = (bucket.cacheWriteTokens ?? 0) + observation.totals.cacheWriteTokens;
|
|
102
|
+
}
|
|
103
|
+
if (observation.totals.reasoningTokens !== undefined) {
|
|
104
|
+
bucket.reasoningTokens = (bucket.reasoningTokens ?? 0) + observation.totals.reasoningTokens;
|
|
105
|
+
}
|
|
106
|
+
bucket.turns += 1;
|
|
107
|
+
if (observation.sessionId !== undefined)
|
|
108
|
+
bucket.sessions.add(observation.sessionId);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"packageManager": "pnpm@11.21.0",
|
|
5
5
|
"description": "Config-driven lifecycle hooks plugin for DeepSeek Harness: declare event -> command hooks in cordis.patch.yml, no plugin code required. Includes a Hooks section in the Web GUI settings (history timeline + manual tester + notify tests + hook editor + Feishu connect).",
|
|
6
6
|
"author": "PeterBon",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
70
70
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
71
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
71
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
72
72
|
"react": "^18.2.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
|
|
78
78
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
|
|
79
79
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.8",
|
|
80
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
80
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
81
81
|
"@tsdown/css": "^0.22.14",
|
|
82
|
-
"@types/node": "^26.
|
|
82
|
+
"@types/node": "^26.4.1",
|
|
83
83
|
"@types/react": "~18.3.1",
|
|
84
84
|
"@types/react-dom": "^18.3.5",
|
|
85
85
|
"react": "^18.3.1",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"vitest": "^4.1.11"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@larksuiteoapi/node-sdk": "^1.73.
|
|
92
|
+
"@larksuiteoapi/node-sdk": "^1.73.1",
|
|
93
93
|
"qrcode": "^1.5.4",
|
|
94
94
|
"yaml": "^2.9.0"
|
|
95
95
|
}
|