@chenmiao8563/dsh-token-ledger 0.1.1 → 0.2.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/CHANGELOG.md +27 -1
- package/README.md +38 -1
- package/README.zh.md +31 -1
- package/docs/VERIFICATION.md +77 -0
- package/lib/client.js +678 -0
- package/lib/index.js +24 -0
- package/lib/overview.js +228 -0
- package/lib/route.js +141 -0
- package/package.json +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,31 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
Nothing yet.
|
|
10
10
|
|
|
11
|
+
## [0.2.0] - 2026-09-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **A settings page.** The browser half registers a `settings.section`, so the
|
|
16
|
+
ledger appears in the settings sidebar beside every other section. It shows:
|
|
17
|
+
- range totals for **this month**, **this year** and the **last 7 days**, each
|
|
18
|
+
with total tokens, cache hit rate and call count, plus the bucket breakdown;
|
|
19
|
+
- **today**, live, which moves as steps complete;
|
|
20
|
+
- a **usage calendar** switchable between year, month and week, where the week
|
|
21
|
+
view is a bar chart rather than a heatmap;
|
|
22
|
+
- the per-model breakdown with its own cache hit rates.
|
|
23
|
+
- `lib/overview.js`, a pure function from a ledger snapshot to the payload the
|
|
24
|
+
page renders, and `lib/route.js`, the read-only
|
|
25
|
+
`GET /api/token-ledger/summary` route that serves it.
|
|
26
|
+
- The browser half is hand-written against the client's module loader and
|
|
27
|
+
requires nothing but `react`, so the package still installs with no build step.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- The overview is served over an HTTP route rather than a settings namespace.
|
|
32
|
+
A namespace would need a schema — a real dependency — and would rewrite
|
|
33
|
+
`settings.yaml` on every debounce with data that is derived and reproducible.
|
|
34
|
+
The ledger file remains the only store of record.
|
|
35
|
+
|
|
11
36
|
## [0.1.1] - 2026-09-10
|
|
12
37
|
|
|
13
38
|
Both fixes come from mounting the plugin in a real DSH host, which is the only
|
|
@@ -63,6 +88,7 @@ so both now have regression tests.
|
|
|
63
88
|
- Zero runtime dependencies, zero peer dependencies and no install scripts, so
|
|
64
89
|
the package installs without a build step.
|
|
65
90
|
|
|
66
|
-
[Unreleased]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.
|
|
91
|
+
[Unreleased]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.2.0...HEAD
|
|
92
|
+
[0.2.0]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.1.1...v0.2.0
|
|
67
93
|
[0.1.1]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.1.0...v0.1.1
|
|
68
94
|
[0.1.0]: https://github.com/chenmiao8563/dsh-token-ledger/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -144,6 +144,33 @@ audit useless. The ledger's own `updatedAt` settles it:
|
|
|
144
144
|
(1 calls, 5100 tokens not yet flushed)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## Settings page
|
|
148
|
+
|
|
149
|
+
The browser half adds a **Token ledger** section to the settings sidebar:
|
|
150
|
+
|
|
151
|
+
- **Range totals** for this month, this year and the last 7 days — total tokens,
|
|
152
|
+
cache hit rate, call count, and the bucket breakdown behind them. The cache hit
|
|
153
|
+
rate is `cacheRead / (cacheRead + uncachedInput)`: the share of input the prompt
|
|
154
|
+
cache absorbed, so a route with no caching reads 0% rather than undefined.
|
|
155
|
+
- **Today, live** — today's tokens, hit rate and calls, which move as steps
|
|
156
|
+
complete.
|
|
157
|
+
- **A usage calendar** switchable between year, month and week. Year and month are
|
|
158
|
+
heatmaps; the week view is a bar chart. Heat levels are relative to the busiest
|
|
159
|
+
day in the window and square-rooted, so one huge day cannot flatten the rest.
|
|
160
|
+
- **Per model** totals, each with its own hit rate.
|
|
161
|
+
|
|
162
|
+
The page reads one read-only route, `GET /api/token-ledger/summary`, and polls it
|
|
163
|
+
every 15 seconds. The route refuses a non-loopback peer, so it stays private even
|
|
164
|
+
if the web server is bound to `0.0.0.0`.
|
|
165
|
+
|
|
166
|
+
It needs a profile with a web server (the `web` or `desktop` profile). Without one,
|
|
167
|
+
`/tokens` and the CLI still work, and the section says so instead of failing.
|
|
168
|
+
|
|
169
|
+
The overview is deliberately **not** published through a settings namespace. That
|
|
170
|
+
would need a schema — a real dependency, and this package has none — and would
|
|
171
|
+
rewrite `settings.yaml` on every debounce with data that is derived and
|
|
172
|
+
reproducible. The ledger file stays the only store of record.
|
|
173
|
+
|
|
147
174
|
## Configuration
|
|
148
175
|
|
|
149
176
|
Override the composition entry by its `id`:
|
|
@@ -186,7 +213,8 @@ The ledger file is left alone on purpose — delete
|
|
|
186
213
|
## Development
|
|
187
214
|
|
|
188
215
|
```bash
|
|
189
|
-
npm
|
|
216
|
+
npm install # devDependencies only: react, react-dom, and the slot registry DSH runs
|
|
217
|
+
npm test # 106 tests
|
|
190
218
|
npm run verify # packaging invariants (dependency-free, no install scripts, no bare imports)
|
|
191
219
|
```
|
|
192
220
|
|
|
@@ -194,6 +222,15 @@ npm run verify # packaging invariants (dependency-free, no install scripts,
|
|
|
194
222
|
spawning a child process per test file is blocked, use
|
|
195
223
|
`npm run test:single-process`.
|
|
196
224
|
|
|
225
|
+
The devDependencies are **test-only**. They are never installed for a consumer:
|
|
226
|
+
the package ships with no dependencies, no peer dependencies and no install
|
|
227
|
+
scripts, and `pnpm` does not install a dependency's devDependencies. They exist
|
|
228
|
+
so the browser half can be checked against the real thing — React renders it, so
|
|
229
|
+
a hook-order or DOM-prop mistake fails instead of passing silently, and
|
|
230
|
+
`@deepseek-ai/dsh-client-ui-slots` validates the registration against the very
|
|
231
|
+
registry that DSH runs. Without them installed the affected tests skip with a
|
|
232
|
+
stated reason, so `npm test` still works from a fresh clone with no network.
|
|
233
|
+
|
|
197
234
|
See [docs/VERIFICATION.md](docs/VERIFICATION.md) for what was actually verified
|
|
198
235
|
and how, including the evidence behind the fork rule.
|
|
199
236
|
|
package/README.zh.md
CHANGED
|
@@ -136,6 +136,29 @@ dsh-token-ledger export [选项] 导出 CSV 与 JSON
|
|
|
136
136
|
(1 calls, 5100 tokens not yet flushed)
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
## 设置页
|
|
140
|
+
|
|
141
|
+
浏览器端会在设置侧边栏注册一个 **用量账本** 分区:
|
|
142
|
+
|
|
143
|
+
- **区间总计**:本月 / 本年 / 近 7 天三档可切换,每档显示 Token 总计、缓存命中率、
|
|
144
|
+
调用次数,以及背后的四桶明细。缓存命中率定义为
|
|
145
|
+
`缓存读入 / (缓存读入 + 未命中输入)`——即输入中被缓存吸收的比例,因此完全不缓存的
|
|
146
|
+
路由读数是 0%,而不是空白。
|
|
147
|
+
- **今日实时**:今天的 Token、命中率与调用次数,随每一步完成而变动。
|
|
148
|
+
- **用量热力图**:年 / 月 / 一周三档可切换;年和月是热力图,切到一周改为柱状图。
|
|
149
|
+
热力档位相对窗口内最忙的一天取平方根,避免某一天特别大把其余全部压成最淡档。
|
|
150
|
+
- **按模型**:各模型的用量与各自命中率。
|
|
151
|
+
|
|
152
|
+
页面只读一个路由 `GET /api/token-ledger/summary`,每 15 秒轮询一次。
|
|
153
|
+
该路由会拒绝非回环来源,因此即使 web 服务器绑定到 `0.0.0.0` 也不会外泄。
|
|
154
|
+
|
|
155
|
+
它需要带 web 服务器的 profile(`web` 或 `desktop`)。没有的话 `/tokens` 与 CLI
|
|
156
|
+
照常可用,分区会明确说明而不是直接失败。
|
|
157
|
+
|
|
158
|
+
概览**刻意不**通过设置命名空间下发:那需要 schema(一个真实依赖,而本包零依赖),
|
|
159
|
+
而且每次防抖都会用可推导、可重放的数据重写一遍 `settings.yaml`。
|
|
160
|
+
账本文件始终是唯一的记录来源。
|
|
161
|
+
|
|
139
162
|
## 配置
|
|
140
163
|
|
|
141
164
|
按 `id` 覆盖组合条目:
|
|
@@ -172,13 +195,20 @@ dsh plugin --profile web remove @chenmiao8563/dsh-token-ledger
|
|
|
172
195
|
## 开发
|
|
173
196
|
|
|
174
197
|
```bash
|
|
175
|
-
npm
|
|
198
|
+
npm install # 两个 devDependency:react 与 react-dom,供渲染测试使用
|
|
199
|
+
npm test # 96 个测试
|
|
176
200
|
npm run verify # 打包不变式(零依赖、无安装脚本、无裸模块说明符)
|
|
177
201
|
```
|
|
178
202
|
|
|
179
203
|
`npm test` 使用 Node 内置测试运行器。在禁止逐文件 spawn 子进程的受限环境里,
|
|
180
204
|
改用 `npm run test:single-process`。
|
|
181
205
|
|
|
206
|
+
React **只是 devDependency**,消费者永远不会安装它:本包不带任何依赖、任何 peer
|
|
207
|
+
依赖、任何安装脚本,而 pnpm 不会为依赖安装其 devDependencies。它在这里的作用是让
|
|
208
|
+
浏览器端能用**真库**渲染并断言——这能抓到替身抓不到的东西:hook 顺序违规与非法
|
|
209
|
+
DOM 属性,React 会报出来,而手写的 `createElement` 会默默接受。没装它时这些渲染
|
|
210
|
+
测试会带明确原因**跳过而不是失败**,所以全新克隆、无网络也能跑 `npm test`。
|
|
211
|
+
|
|
182
212
|
实际验证了什么、怎么验证的(包括 fork 规则背后的证据)见
|
|
183
213
|
[docs/VERIFICATION.md](docs/VERIFICATION.md)。
|
|
184
214
|
|
package/docs/VERIFICATION.md
CHANGED
|
@@ -220,6 +220,83 @@ sessions against the 72 logs carrying a `session/end-seed` marker and the 29
|
|
|
220
220
|
declaring a parent. The 61 sessions outside both sets turned out to be
|
|
221
221
|
four-record logs with no usage at all, which is why they correctly have no entry.
|
|
222
222
|
|
|
223
|
+
## Browser half (0.2.0)
|
|
224
|
+
|
|
225
|
+
What is checked here and what is not, because the difference is large.
|
|
226
|
+
|
|
227
|
+
**Checked by test.** `lib/overview.js` and `lib/route.js` are pure or near-pure
|
|
228
|
+
and are tested directly: range boundaries, cache-hit-rate arithmetic, gap-filled
|
|
229
|
+
series, the loopback and origin guard, method handling, and the 500-on-fault
|
|
230
|
+
path. The browser half is loaded through a stand-in module loader with a
|
|
231
|
+
stand-in React, which exercises the module wrapper, the registration contract,
|
|
232
|
+
and the whole render tree — formatting, heat levels, week-bar slicing, view
|
|
233
|
+
switching, and the empty, malformed and stale-data paths.
|
|
234
|
+
`scripts/verify-package.mjs` additionally fails the build if the browser half
|
|
235
|
+
requires anything the loader does not provide, if its module id stops matching
|
|
236
|
+
the package name, if it stops compiling as a classic script, or if a declared
|
|
237
|
+
client dependency is outside the client-half namespace.
|
|
238
|
+
|
|
239
|
+
**Checked by reading DSH's own source, not by inference.** The contracts the
|
|
240
|
+
browser half depends on were read out of the shipped bundles rather than copied
|
|
241
|
+
from a third-party plugin:
|
|
242
|
+
|
|
243
|
+
| Contract | Where it was read | Result |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| The `settings.section` registration shape | the sections DSH ships — `dsh-client-ui-settings-models` (order 10), `-settings-plugins` (15) and `-ui-agent-preset` (20) — plus the shell in `-settings-general` that renders them via `renderSlot("settings.section", …, { only: active })` | identical field set; `locale` is optional, proven by `-settings-models` omitting it; the sidebar rows come from `id`, `order` and `label`, all of which this plugin supplies |
|
|
246
|
+
| The client module format | every `@deepseek-ai/dsh-client-*/lib/client.js` | byte-identical wrapper: `window.__ModuleLoader__.load({ id, factory })` with `var module = { exports: {} }` and `exports.apply`/`exports.inject` |
|
|
247
|
+
| When the factory runs | `dsh-client-modules` module docs | only registration happens at script execution; body side effects, styles included, run at materialization — which is why the stylesheet is installed inside `apply()` |
|
|
248
|
+
| The `dsh.client` declaration | `parseDshClient` in `dsh-client-modules/lib/index.js` | `platform` must be a string and only `web` is loaded; `inject` and `external` are optional string arrays; `external` — not `inject` — is what orders the module graph |
|
|
249
|
+
| What `dsh.client.inject` **contains** | the five `dsh.client` declarations DSH ships, two of them non-empty: `dsh-client-locale` and `dsh-client-ui-settings-general` | **package/entry ids, not service names.** A module's own exported `inject` is the separate service list. This plugin lists the two packages whose services it uses, which is the same convention, and an empty list is also legal (`dsh-client-connection` uses one) |
|
|
250
|
+
| What `external` contains | the module-graph walk in `dsh-client-modules/lib/client.js`, which runs every entry through `stripClientSuffix` before resolving it | module specifiers for *other rows*. This plugin requires only `react`, which the loader provides and no row owns, so it declares none |
|
|
251
|
+
| How `exports["./client"]` is located | `clientExportOf` in `dsh-client-modules/lib/index.js` | a plain string or an object with a string `default`; anything else throws. A plain string is used here, and `verify-package.mjs` pins it |
|
|
252
|
+
| Whether a **scoped** package id survives | `stripClientSuffix` in `dsh-client-modules/lib/client.js` | it removes only a trailing `/client`, so `@chenmiao8563/dsh-token-ledger` passes through intact. DSH's own client packages are scoped too, so the transport already carries ids containing `/` |
|
|
253
|
+
|
|
254
|
+
That reading corrected two real mistakes in the first draft of this half, both of
|
|
255
|
+
which would have been a silent failure rather than an error:
|
|
256
|
+
|
|
257
|
+
- `@deepseek-ai/dsh-client-ui-slots` was listed as a client dependency. It is a
|
|
258
|
+
pure core library (its manifest exports only `.`, and it declares no
|
|
259
|
+
`dsh.client`), so it is never a client module row.
|
|
260
|
+
- The settings shell package was listed as `-ui-settings`, which holds the
|
|
261
|
+
namespace-scope service; the section slot is declared and rendered by
|
|
262
|
+
`-ui-settings-general`.
|
|
263
|
+
|
|
264
|
+
**Checked under the real React.** `test/client-render.test.mjs` renders the
|
|
265
|
+
presentation component with React 18.3.1 — the same major version DSH bundles —
|
|
266
|
+
through `react-dom/server`, and asserts the actual markup: the range tabs and
|
|
267
|
+
their active state, the totals, hit rate and call count for each range, today's
|
|
268
|
+
block surviving a year-range selection, the year heat grid (one cell per day plus
|
|
269
|
+
five legend swatches), the month grid (every day of March), the week bars (seven,
|
|
270
|
+
with inline heights), bounded heat levels drawn from a five-entry palette,
|
|
271
|
+
proportional model bars, and the loading, error, stale and empty states. It also
|
|
272
|
+
captures `console.error` and `console.warn` and **fails if React complained at
|
|
273
|
+
all**, which is what catches an invalid DOM prop or a hook-order violation — the
|
|
274
|
+
class of defect a hand-rolled `createElement` accepts silently.
|
|
275
|
+
|
|
276
|
+
React is a devDependency, so this runs in CI and under `prepublishOnly`. Without
|
|
277
|
+
it installed the file skips with a stated reason rather than failing, which keeps
|
|
278
|
+
`npm test` working from a fresh clone with no network.
|
|
279
|
+
|
|
280
|
+
**Checked against the real slot registry.** `test/slot-registration.test.mjs`
|
|
281
|
+
feeds the registration `apply` actually builds into
|
|
282
|
+
`@deepseek-ai/dsh-client-ui-slots` — the registry DSH itself runs, pinned as a
|
|
283
|
+
devDependency to `0.1.2-rc.1`, the exact version this DSH bundles (`latest` on
|
|
284
|
+
npm is an older `0.0.1-rc.1`, so the version is pinned rather than ranged). It
|
|
285
|
+
asserts that the registry accepts the entry, that `resolveSlotLabel` resolves the
|
|
286
|
+
label thunk to a non-empty string — a label it could not resolve would render as
|
|
287
|
+
a blank sidebar row — that the entry id does not collide with the three sections
|
|
288
|
+
DSH ships, that a duplicate id is refused, that disposing retires the entry, and
|
|
289
|
+
that registering into an undeclared slot is refused, which is why the plugin uses
|
|
290
|
+
`slots.inject` rather than registering blind.
|
|
291
|
+
|
|
292
|
+
**Not checked.** Whether the client module loader accepts the file, whether the
|
|
293
|
+
shell renders the section, and how any of it looks. Nothing here executes
|
|
294
|
+
`window.__ModuleLoader__` or mounts the component into a DOM: the module's shape
|
|
295
|
+
is asserted to match every bundle DSH ships, and the registration is validated by
|
|
296
|
+
the real registry, but no single process loads the plugin the way the browser
|
|
297
|
+
does. The remaining risk is integration and visual — a first-run surprise, not a
|
|
298
|
+
contractual one.
|
|
299
|
+
|
|
223
300
|
## Not verified
|
|
224
301
|
|
|
225
302
|
Stated plainly, because a verification file that only lists successes is not
|