dsh-plugin-usage-stats 0.4.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/LICENSE +21 -0
- package/README.md +51 -0
- package/README.zh.md +51 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +716 -0
- package/package.json +71 -0
- package/src/aggregate.ts +175 -0
- package/src/cordis.ts +72 -0
- package/src/scanner.ts +391 -0
- package/src/store.ts +152 -0
- package/tests/contract-pair.test.ts +108 -0
- package/tests/cordis.test.ts +96 -0
- package/tests/fold.test.ts +122 -0
- package/tests/realscan.test.ts +24 -0
- package/tests/scan.test.ts +168 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ksxh0524
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# dsh-plugin-usage-stats
|
|
2
|
+
|
|
3
|
+
[中文](./README.zh.md)
|
|
4
|
+
|
|
5
|
+
A usage-statistics plugin for the DSH (DeepSeek Harness) Web GUI (v3): a **dedicated "Token 用量" page in Settings** — a global, session-independent report across all workspaces, with a date-range picker (defaults to today; presets 今天 / 近3天 / 近7天 / 近30天 / 全部), **model and provider filters**, a KPI grid and one per-model table. Strictly read-only, zero instrumentation.
|
|
6
|
+
|
|
7
|
+
Per-session stats (turns/steps, tok/s, cache hit, per-message usage) are **built into the host chat UI** — this plugin deliberately does not duplicate them; v2's sidebar tab and drill-down endpoints were removed for that reason.
|
|
8
|
+
|
|
9
|
+
- Data source: `<DSH_HOME>/sessions/*/*/session.v3.jsonl.zstd` — a **global view across all workspaces**, including subagent sessions.
|
|
10
|
+
- Incremental by design: session files are appended multi-frame zstd streams. A byte-accurate zstd frame walker (RFC 8878 headers, no LZ4 decoding) lets the scanner persist fold state per file and, on later runs, decompress **only newly completed frames**; unchanged files cost zero I/O. Persistent state lives in `<DSH_HOME>/cache/usage-stats.folds.json` (atomic tmp+rename; corrupt or missing state falls back to a full rescan).
|
|
11
|
+
- Shape: the server side registers a `usageStats` Typert Remote with a **single read-only method `overview(filter)`**; the browser half is a hand-written `__ModuleLoader__` factory (`lib/client.js`, no build chain) that self-mounts its remote descriptor and injects a `settings.section` entry.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
dsh plugin --profile <your-profile> add dsh-plugin-usage-stats
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then **restart that profile's host** (newly mounted packages are not hot-loaded). Reload the Web GUI → Settings → General sidebar → **Token 用量**.
|
|
20
|
+
|
|
21
|
+
- **Date range**: one trigger button (never two native inputs) opening a popover: preset chips + a month calendar for arbitrary ranges (local time zone, day granularity). "全部" clears the window.
|
|
22
|
+
- **Filters**: provider and model dropdowns (options derived from the last unfiltered scan; picking a provider narrows the model list).
|
|
23
|
+
- **Report**: sessions scanned, total tokens, uncached input / output / cache read (with hit rate) / cache write, then a single per-model table — deliberately **no per-day table**: the date-range filter already answers "what about this day" (set the range to that day). Deliberately lightweight: the report is pure token statistics — money left the product in v4 (the whole price/pricing layer was retired, API included); the footer is just a data-freshness timestamp. Number tiers: exact under 1K, then `K` (≥1K, one decimal), `M` (≥1M, two decimals), `B` (≥100M, two decimals) — trailing zeros trimmed.
|
|
24
|
+
|
|
25
|
+
## Metrics
|
|
26
|
+
|
|
27
|
+
- `inputTokens` = **uncached input** (`total = input + output + cacheRead + cacheWrite`, verified against real data).
|
|
28
|
+
- Hit rate = `cacheRead / (cacheRead + uncached input)`; `cacheWrite` is reported separately and excluded from the ratio.
|
|
29
|
+
- Providers that never report cache fields show the hit rate as "—" (never a misleading 0%).
|
|
30
|
+
- Retry folding: within one `(session, turn, step)` scope only the last usage sample survives — which also makes incremental replay idempotent.
|
|
31
|
+
- Session profile: the scanner still parses `userMessages` / `assistantMessages` / `toolCalls` into `SessionMeta` (cheap inline counting), but the overview no longer aggregates them — the UI shows no message row.
|
|
32
|
+
- `overview(filter)` accepts `{ from?, to?, model?, provider? }` — dates are `YYYY-MM-DD` (local zone, inverted pairs are swapped), `model` matches the `"provider/model"` key exactly or a bare model name as fallback, `provider` matches the provider segment. Invalid keys are dropped, never guessed.
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
When mounted into a host profile via pnpm `link:` (a symlink), source edits need **no reinstall**: server-side changes apply after a host restart, `lib/client.js` changes apply on page reload. With `file:` mounting you must run `pnpm install` inside the profile to resync the copy each time.
|
|
37
|
+
|
|
38
|
+
## Tests
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
node --test tests/*.test.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Fixture metric tests (folding, retry replacement, filters, session-profile counts, store version gate), byte-accurate frame-walker tests against real `zstd` CLI output (skipped without the CLI), an incremental-equals-full-replay property test with lines deliberately split across frame boundaries, a persistent-store restart test, plus integration tests against the real session directory (auto-skipped when none exists).
|
|
45
|
+
|
|
46
|
+
## Known limits (v0.4)
|
|
47
|
+
|
|
48
|
+
- Incremental decoding relies on the frame walk recognizing completed frames; a half-written tail frame is folded on the next run, and legacy/dict frames degrade to full-file rescans (still correct, just slower).
|
|
49
|
+
- Without the `zstd` CLI the incremental path decodes frame-by-frame via node:zlib; if that fails too, the whole-store fallback is a correct full rescan.
|
|
50
|
+
- Fetch on open and on manual refresh / filter change; there is no server push, and no automatic polling.
|
|
51
|
+
- The browser side `$mount`s a hand-written strict descriptor (result schema is passthrough); the method/parameter names (`overview(filter)`) are an implicit contract shared with `src/cordis.ts` — renaming on one end must be synced to the other.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# dsh-plugin-usage-stats
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
DSH(DeepSeek Harness)Web GUI 的用量统计插件(v3):**设置里的独立「Token 用量」页**——跨全部 workspace 的全局报表(与会话无关):日期范围选择器(默认今天;预设 今天 / 近3天 / 近7天 / 近30天 / 全部)、**按模型与服务商过滤**、KPI 网格与一张按模型明细表。严格只读,零埋点。
|
|
6
|
+
|
|
7
|
+
单会话统计(轮/步、tok/s、缓存命中、逐条消息用量)**宿主聊天界面自带**——本插件刻意不重复造;v2 的右侧栏 tab 与下钻接口因此删除。
|
|
8
|
+
|
|
9
|
+
- 数据源:`<DSH_HOME>/sessions/*/*/session.v3.jsonl.zstd`——**跨全部 workspace 的全局视图**,含子代理会话。
|
|
10
|
+
- 增量设计:会话文件是追加式多帧 zstd。字节级精确的帧 walker(RFC 8878 帧头/块头,不碰 LZ4)让扫描器按文件持久化折叠状态,后续只解压**新完成的帧**,未变文件零解压开销。状态落 `<DSH_HOME>/cache/usage-stats.folds.json`(tmp+rename 原子写;状态损坏或缺失自动整文件重扫)。
|
|
11
|
+
- 形态:服务端注册 `usageStats` Typert Remote(**单只读方法 `overview(filter)`**);浏览器半是手写 `__ModuleLoader__` 工厂(`lib/client.js`,无构建链),自挂 remote descriptor 并注入 `settings.section` 页面。
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
dsh plugin --profile <your-profile> add dsh-plugin-usage-stats
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
然后**重启该 profile 的 host**(新挂载的包不会热加载)。刷新 Web GUI → 设置 → 通用侧栏 → **Token 用量**。
|
|
20
|
+
|
|
21
|
+
- **日期范围**:单触发按钮(不是两个原生输入框),弹层内含预设快捷键 + 月历任意范围选择(本地时区、按天粒度)。点「全部」清空窗口。
|
|
22
|
+
- **过滤**:服务商、模型两个下拉(选项池来自最近一次无过滤扫描;选中服务商后模型列表随之收窄)。
|
|
23
|
+
- **报表**:会话数 / 总 token / 未缓存输入 / 输出 / 缓存读(含命中率)/ 缓存写,随后仅一张按模型明细表。刻意**不做按天表**——看某一天用日期筛选直接框住那天即可;报表是纯 token 统计——费用在 v4 整个退出产品(价目层连 API 一并拆除),底部只标数据截至时间。数字刻度:不到 1K 写具体数,之后 `K`(≥1K,1 位小数)→ `M`(≥1M,2 位)→ `B`(≥100M,2 位),尾零去除。
|
|
24
|
+
|
|
25
|
+
## 口径
|
|
26
|
+
|
|
27
|
+
- `inputTokens` = **未缓存输入**(`total = input + output + cacheRead + cacheWrite`,真实数据已核实)。
|
|
28
|
+
- 命中率 = `cacheRead / (cacheRead + 未缓存输入)`;`cacheWrite` 单列报告、不参与比率。
|
|
29
|
+
- 从不上报缓存字段的 provider,命中率显示“—”(绝不给误导性的 0%)。
|
|
30
|
+
- 重试折叠:同一 `(session, turn, step)` scope 只保留末条 usage 样本——这也让增量重放天然幂等。
|
|
31
|
+
- 会话画像:scanner 仍把 `userMessages` / `assistantMessages` / `toolCalls` 解析进 `SessionMeta`(逐行 O(1) 顺带计数),但 overview 不再聚合——界面无消息行。
|
|
32
|
+
- `overview(filter)` 接受 `{ from?, to?, model?, provider? }`——日期 `YYYY-MM-DD`(本地时区,起止倒挂自动交换),`model` 匹配(`"provider/model"` 全键精确优先、裸模型名兜底),`provider` 匹配服务商段。非法键直接丢弃、不做猜测。
|
|
33
|
+
|
|
34
|
+
## 开发
|
|
35
|
+
|
|
36
|
+
以 pnpm `link:`(符号链接)挂进宿主 profile 时,改源码**免重装**:服务端改动重启 host 生效,`lib/client.js` 改动刷新页面即生效。`file:` 挂载则每次须在 profile 内 `pnpm install` 重同步拷贝。
|
|
37
|
+
|
|
38
|
+
## 测试
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
node --test tests/*.test.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
fixture 口径测试(折叠、重试取末条、维度过滤、会话画像计数、store 版本门)、用真 `zstd` CLI 产物对表的帧边界测试(无 CLI 自动 skip)、跨帧半行场景的「增量 === 全量重放」性质测试、持久化 store 重启复用测试,另有真实会话目录集成测试(无会话目录自动 skip)。
|
|
45
|
+
|
|
46
|
+
## 已知边界(v0.4)
|
|
47
|
+
|
|
48
|
+
- 增量解码依赖帧 walker 识别「完整帧」:文件尾半帧留待下次补齐;legacy/字典帧退化为整文件重扫(仍正确,只是慢)。
|
|
49
|
+
- 无 `zstd` CLI 时增量按帧喂 node:zlib;再失败则整库回退全量重解(结果正确)。
|
|
50
|
+
- 打开页面、手动刷新、改筛选时拉取;无服务端推送,也不自动轮询。
|
|
51
|
+
- 浏览器侧 `$mount` 手写 strict descriptor(结果 schema 透传);方法/参数名(`overview(filter)`)与 `src/cordis.ts` 是隐式两端契约,改一端必同步另一端。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# dsh-plugin-usage-stats layer:用量统计服务(server)+ 浏览器面板(client,dsh.client 行由同一包承载)。
|
|
2
|
+
# config.sessionsHome:置空串 = 用默认($DSH_HOME/sessions 或 ~/.dsh/sessions);用户 profile patch 层可覆盖指向其他会话根。
|
|
3
|
+
- insert:
|
|
4
|
+
- id: usage-stats
|
|
5
|
+
name: dsh-plugin-usage-stats
|
|
6
|
+
config:
|
|
7
|
+
sessionsHome: ""
|