dsh-consumption-badge 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +244 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +2474 -0
- package/lib/index.js +385 -0
- package/package.json +43 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zlc13
|
|
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
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-consumption-badge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A **persistent** DSH (DeepSeek Harness) Web UI plugin that puts a live consumption badge
|
|
4
|
+
next to every assistant message, and opens a detail panel with the session's spend curve.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
⛽ 25.6k | ≈¥0.020 | valley
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- **Collapsed pill** – session-cumulative tokens, estimated cost (always prefixed `≈`), and the
|
|
11
|
+
current peak/valley tier (red `peak`, green `valley`), injected into
|
|
12
|
+
`conversation.chat.assistant-actions` right after the shipped copy/branch controls.
|
|
13
|
+
- **Detail modal** (centered, blurred backdrop, `Esc` or backdrop-click to close):
|
|
14
|
+
cumulative tokens with hit/miss/output split, estimated cost, current pricing tier,
|
|
15
|
+
DeepSeek account balance, and a **per-turn dual-axis line chart**
|
|
16
|
+
(left axis: tokens, right axis: ¥) with a hover tooltip showing the model,
|
|
17
|
+
input-hit / input-miss / output tokens, and the turn's peak/valley flag.
|
|
18
|
+
- **Settings page · 消耗** (`settings.section`, id `balance-dsh-consumption`, order 26):
|
|
19
|
+
a top row of four cards — **累计 Token** (with hit/miss/output split, all-history totals),
|
|
20
|
+
**估算金额**, **当前时段** (peak/idle) and **账户余额** (reusing the badge's cached balance) —
|
|
21
|
+
then **all-session history** as one line chart, filterable by time range
|
|
22
|
+
(today / last 7 days / last 30 days / last 12 months), by workspace
|
|
23
|
+
(全部工作区 + every registered workspace + 未归属工作区), and by an explicit
|
|
24
|
+
calendar range that overrides the dropdown. Bucket size follows the range:
|
|
25
|
+
**hourly for today and any single-day calendar range**, daily for multi-day ranges, and
|
|
26
|
+
monthly for the last 12 months. Left axis (tokens) is hidden until you
|
|
27
|
+
toggle it in the legend; the right axis (¥) is always on. Hovering a point shows its
|
|
28
|
+
total, its peak/valley split and the models involved; the footer sums the current
|
|
29
|
+
selection. No data in range → a plain “该范围内暂无消耗记录” note instead of an empty chart.
|
|
30
|
+
The cards are **all-history** totals (independent of the filters); the footer is the
|
|
31
|
+
filtered total, so the two numbers differ by design.
|
|
32
|
+
- **Personalization integration** (auto-detected, no configuration): when
|
|
33
|
+
[`dsh-plugin-personalization`](https://www.npmjs.com/package/dsh-plugin-personalization) is
|
|
34
|
+
installed alongside this plugin, a **消耗弹窗** block is appended to its settings page —
|
|
35
|
+
same `pp-block` / `pp-field` / `pp-modes` layout and the same `Pill` primitive as the shipped
|
|
36
|
+
personalization rows — offering four backgrounds for the consumption modal:
|
|
37
|
+
**默认 / 毛玻璃 / 自定义背景 / 纯色**.
|
|
38
|
+
- **默认** keeps the themed surface; **毛玻璃** is real `backdrop-filter` glass — both the veil behind
|
|
39
|
+
the modal and the panel itself are theme-derived (`--dsw-alias-bg-base`), so light themes get a light
|
|
40
|
+
haze and dark themes a dark one instead of the panel painting a dark slab over the page;
|
|
41
|
+
**自定义背景** takes an image URL *or* a local image plus an **不透明度** slider (0–100 %, default
|
|
42
|
+
85 %) that sets the background layer's own `opacity` — the same semantic as the other
|
|
43
|
+
personalization background rows, so lowering it fades the image over the themed panel instead of
|
|
44
|
+
dimming it; **纯色** takes a hex colour and derives a light/dark ink palette so the
|
|
45
|
+
cards, chart and tooltips stay readable (the palette also overrides the tooltip surface).
|
|
46
|
+
- A **毛玻璃** switch appears for 自定义背景 and 纯色, so glass composites with them:
|
|
47
|
+
on an image it blurs the image itself (frosted photo), on a solid it makes the colour
|
|
48
|
+
translucent over a blurred page. A **毛玻璃强度** slider (0–48 px, default 22 px) sets the
|
|
49
|
+
blur radius for both the panel and the veil behind it.
|
|
50
|
+
- **清除** wipes the image, the URL field and the file picker in one go. The image row shows a
|
|
51
|
+
**thumbnail** plus the live save state
|
|
52
|
+
(正在保存 / 已保存到本机 / `error: …`), and failures are logged with a
|
|
53
|
+
`[consumption-badge]` tag instead of failing silently. The picker is a button
|
|
54
|
+
(“选择图片… / 更换图片…”), not a raw file input, so saving never leaves a misleading
|
|
55
|
+
“未选择文件” label behind.
|
|
56
|
+
- Local images are **stored on this machine with no size limit**: the picked file is written as a
|
|
57
|
+
Blob into IndexedDB (`dsh-consumption-assets` → `images`), and the preference only keeps a small
|
|
58
|
+
key — so a 20 MB wallpaper no longer blows the `localStorage` quota. On startup the blob is read
|
|
59
|
+
back into a `blob:` URL for the modal; **清除** deletes the stored record as well.
|
|
60
|
+
If IndexedDB is unavailable the file is read as a data URL instead, and the row says so.
|
|
61
|
+
- The choice is a local preference (`dsh-consumption:modal-style`) and is applied to the open
|
|
62
|
+
modal immediately. Nothing about the conversation or sidebar backgrounds is touched.
|
|
63
|
+
- **Update notice** (same shape as `dsh-plugin-personalization`'s): ~2.5 s after the page loads the
|
|
64
|
+
plugin asks `https://registry.npmjs.org/dsh-consumption-badge/latest` for the newest version
|
|
65
|
+
(24 h cache, 5 s timeout, fails silently). If the installed version is older — and that version
|
|
66
|
+
was not ignored — a non-blocking card appears in the bottom-right corner with
|
|
67
|
+
**current → latest**, the exact `dsh plugin --profile web update dsh-consumption-badge` command,
|
|
68
|
+
and three actions: **复制更新命令**, **忽略此版本** (remembered on this machine) and
|
|
69
|
+
**稍后提醒** (this page load only). It never blocks the conversation and never covers controls.
|
|
70
|
+
|
|
71
|
+
Everything is an **estimate** for reference only — this plugin is not a billing system of record.
|
|
72
|
+
|
|
73
|
+
## Requirements
|
|
74
|
+
|
|
75
|
+
| Capability | Why |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| DSH `0.1.0-rc.7` or compatible | slot API `conversation.chat.assistant-actions`, `shell.overlay` |
|
|
78
|
+
| `DEEPSEEK_API_KEY` in the DSH credential store | account balance (`GET /user/balance`) |
|
|
79
|
+
| `curl` on `PATH` | the balance request; on Windows 10+ it ships as `C:\Windows\System32\curl.exe` |
|
|
80
|
+
|
|
81
|
+
The API key is resolved **per request** through the Cordis `credentials` service and passed to
|
|
82
|
+
`curl` on **stdin** (`curl -K -`), never through argv, and never reaches the browser.
|
|
83
|
+
|
|
84
|
+
If the key or `curl` is missing, only the balance card degrades (it shows the reason);
|
|
85
|
+
tokens, cost and the chart keep working.
|
|
86
|
+
|
|
87
|
+
## Install
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
dsh plugin --profile web add dsh-consumption-badge
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then **restart the DSH process** — a host-side plugin (and the browser plugin roster) is composed
|
|
94
|
+
at process start, so a page refresh is not enough.
|
|
95
|
+
|
|
96
|
+
Verify:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd "$DSH_HOME/profiles/web"
|
|
100
|
+
dsh --profile web --dump-config | grep consumption-badge # expect exactly one row
|
|
101
|
+
# after restart:
|
|
102
|
+
curl -s http://127.0.0.1:<port>/api/consumption/account # expect {"ok":true,...,"model":"..."}
|
|
103
|
+
curl -s -o /dev/null -w '%{http_code}\n' \
|
|
104
|
+
http://127.0.0.1:<port>/plugins/dsh-consumption-badge/client.js # expect 200
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### If the row does not mount
|
|
108
|
+
|
|
109
|
+
Some deployments compose `dsh.profile.bundles` into `--dump-config` without mounting those rows
|
|
110
|
+
at runtime. The reliable path is a roster row in the **profile's own patch layer** — add it to
|
|
111
|
+
`$DSH_HOME/profiles/web/cordis.patch.yml` and remove `dsh-consumption-badge` from
|
|
112
|
+
`dsh.profile.bundles` so the id is inserted exactly once:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- insert:
|
|
116
|
+
- id: consumption-badge
|
|
117
|
+
name: 'dsh-consumption-badge'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The package also ships its own `cordis.patch.yml` (`dsh.bundle.patch`) with the same row for
|
|
121
|
+
deployments where the bundle layer does mount. Keep only **one** of the two active.
|
|
122
|
+
|
|
123
|
+
## What it reads
|
|
124
|
+
|
|
125
|
+
| Data | Source |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| Per-turn tokens | `assistant/message.usage` events in the session log, folded per turn (`cacheReadTokens` → hit, `inputTokens + cacheWriteTokens` → miss, `outputTokens` → output; multi-step turns are summed) |
|
|
128
|
+
| Turn timing | `turn/start` event time (peak/valley is attributed to **request start**) |
|
|
129
|
+
| Model per turn | `request/context` / `request/header.config`; falls back to the agent default model, then `deepseek-flash` |
|
|
130
|
+
| Session totals | the shipped `tokenUsage` projection when present, otherwise the folded turns |
|
|
131
|
+
| Balance | `GET https://api.deepseek.com/user/balance` via curl, 60s cache |
|
|
132
|
+
|
|
133
|
+
Turn records are also mirrored into browser `localStorage` under
|
|
134
|
+
`dsh-consumption:<sessionId>:<turn>` so the chart survives reloads. Persistence writes are
|
|
135
|
+
scheduled off the render path (`requestIdleCallback`), and the aggregation is O(turns).
|
|
136
|
+
|
|
137
|
+
The settings page reads **those same records** (no second store, no new field): workspace
|
|
138
|
+
attribution is resolved at read time from DSH's own accounting ledger
|
|
139
|
+
(`WorkspaceView.sessionIds` from `useWorkspaces`), so historical records are attributed without
|
|
140
|
+
any backfill. Records whose session no longer exists land in the 未归属工作区 bucket.
|
|
141
|
+
Above 5000 records the aggregation runs in a Web Worker (built from the pure aggregator's own
|
|
142
|
+
source, so both paths are the same code); if the browser refuses the Worker it silently falls
|
|
143
|
+
back to the main thread, where 6000 records aggregate in ~6 ms.
|
|
144
|
+
|
|
145
|
+
## Pricing model
|
|
146
|
+
|
|
147
|
+
Off-peak prices in **CNY per 1M tokens**; peak multiplies everything by **2**.
|
|
148
|
+
|
|
149
|
+
| Model | input cache hit | input cache miss | output |
|
|
150
|
+
| --- | --- | --- | --- |
|
|
151
|
+
| `deepseek-flash` | 0.02 | 1 | 4 |
|
|
152
|
+
| `deepseek-v4-pro` | 0.15 | 4.5 | 13.5 |
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
cost = (hit/1e6)*hit_price + (miss/1e6)*miss_price + (out/1e6)*out_price [×2 at peak]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Peak** = Beijing time (UTC+8), Monday–Friday, 09:00–12:00 and 14:00–18:00, excluding statutory
|
|
159
|
+
holidays. Everything else, plus weekends, is **valley**.
|
|
160
|
+
|
|
161
|
+
Unrecognised model ids are priced as `deepseek-flash` (or `deepseek-v4-pro` when the id contains
|
|
162
|
+
`pro`) and the UI marks the figure as an estimate. The table lives in `PRICES` at the top of
|
|
163
|
+
`lib/client.js` — fork and edit to follow a price change.
|
|
164
|
+
|
|
165
|
+
The fixed-date holiday list (`CN_FIXED_HOLIDAYS`, `MM-DD`) covers New Year, Labour Day and the
|
|
166
|
+
first three days of National Day. Lunar holidays (Spring Festival, Qingming, Dragon Boat,
|
|
167
|
+
Mid-Autumn) must be added by hand.
|
|
168
|
+
|
|
169
|
+
## HTTP surface
|
|
170
|
+
|
|
171
|
+
The host half registers three exact, read-only routes on the DSH web server:
|
|
172
|
+
|
|
173
|
+
| Route | Returns |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `GET /api/consumption/turns?sessionId=<id>` | per-turn `{turn, hit, miss, out, time, model}` (log-length cached) |
|
|
176
|
+
| `GET /api/consumption/account` | current provider/model + whether the key is configured |
|
|
177
|
+
| `GET /api/consumption/balance[?force=1]` | `{ok, available, currency, total, granted, toppedUp}`; `is_available:false` renders a red “insufficient balance” |
|
|
178
|
+
|
|
179
|
+
These routes are **unauthenticated**. If your DSH web server binds `0.0.0.0`, anyone on the
|
|
180
|
+
network can read the balance and token figures; keep it on `127.0.0.1` unless you need otherwise.
|
|
181
|
+
|
|
182
|
+
## Prompt for your AI agent (copy-paste)
|
|
183
|
+
|
|
184
|
+
Paste this into Claude Code / Codex / DSH and let it do the install and verification:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
Install the DSH plugin "dsh-consumption-badge" into my web profile and prove it works.
|
|
188
|
+
|
|
189
|
+
1. In $DSH_HOME/profiles/web run: `dsh plugin --profile web add dsh-consumption-badge`.
|
|
190
|
+
(For a local checkout use `dsh plugin --profile web add "file:<abs path to the package>"`.)
|
|
191
|
+
2. Check `dsh --profile web --dump-config` contains exactly ONE `id: consumption-badge` row.
|
|
192
|
+
`dsh plugin add` also appends the package to `dsh.profile.bundles`; if that row does not
|
|
193
|
+
actually mount after the restart, move it: remove it from `dsh.profile.bundles` and add
|
|
194
|
+
`- insert: [{id: consumption-badge, name: 'dsh-consumption-badge'}]` to the profile's
|
|
195
|
+
`cordis.patch.yml` instead. Never leave both.
|
|
196
|
+
3. Restart the DSH process (a browser refresh is NOT enough — host plugins are composed at boot).
|
|
197
|
+
4. Verify the host half: `curl -s http://127.0.0.1:<port>/api/consumption/account` must return
|
|
198
|
+
`{"ok":true,...}`. Verify the client half:
|
|
199
|
+
`curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/plugins/dsh-consumption-badge/client.js`
|
|
200
|
+
must return `200`.
|
|
201
|
+
5. Open any session and confirm the `⛽ <tokens> | ≈¥<cost> | peak|valley` pill in the assistant
|
|
202
|
+
action row; click it and confirm the modal shows the four cards, the dual-axis chart, the
|
|
203
|
+
balance, and that Esc closes it.
|
|
204
|
+
6. Open Settings → 消耗: confirm the history chart renders, then
|
|
205
|
+
switch the time range (今天 / 近一周 / 近一月 / 近一年), switch the workspace
|
|
206
|
+
(全部工作区 → a specific workspace → 未归属工作区), set the calendar range and confirm both
|
|
207
|
+
the chart and the footer totals update, and that the Token axis only appears after clicking
|
|
208
|
+
its legend chip.
|
|
209
|
+
7. If `dsh-plugin-personalization` is installed, open Settings → 个性化 and confirm a 消耗弹窗
|
|
210
|
+
block appears at the bottom; switch it to 毛玻璃 / 纯色 / 自定义背景 and confirm the ⛽ modal
|
|
211
|
+
(click a badge) re-renders with that background.
|
|
212
|
+
8. Report which step failed and paste the boot log lines that mention `consumption-badge`.
|
|
213
|
+
|
|
214
|
+
Rules while doing this: never write to the DSH credentials file yourself (ask me for
|
|
215
|
+
DEEPSEEK_API_KEY), never bind a second server to the same port, and do not try to persist the
|
|
216
|
+
plugin by "running" it — a session-scoped dynamic plugin always dies with the process.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Notes for AI agents
|
|
220
|
+
|
|
221
|
+
- This is a **profile-level** plugin: `package.json` carries `dsh.bundle.patch` and `dsh.client`
|
|
222
|
+
(`platform: web`). It cannot be shipped as a dynamic/session plugin — `harness.handle` and
|
|
223
|
+
`host.call` are dynamic-runner-only and do not exist for a bundled plugin; the client half talks
|
|
224
|
+
to the host half over the same-origin routes listed above.
|
|
225
|
+
- The client bundle is hand-written in `window.__ModuleLoader__.load({id, factory})` form and only
|
|
226
|
+
`require("react")` — React must stay external (a second React instance breaks slot rendering).
|
|
227
|
+
- `lib/index.js` imports nothing at all (no `@deepseek-ai/cordis`), so it does not depend on the
|
|
228
|
+
profile being able to resolve Cordis; it exports plain `name` / `inject` / `apply`.
|
|
229
|
+
- Self-check without booting DSH: `node check.mjs` (46 assertions over the route contract and the
|
|
230
|
+
client bundle contract). It lives outside the published `files` list.
|
|
231
|
+
|
|
232
|
+
## Known limitations
|
|
233
|
+
|
|
234
|
+
- All amounts are **estimates** — price table, peak/valley rules and model detection can drift
|
|
235
|
+
from your actual invoice.
|
|
236
|
+
- One chart point per turn; multiple steps in a turn are summed.
|
|
237
|
+
- Balance parsing depends on DeepSeek's `/user/balance` response shape; failures only affect the
|
|
238
|
+
balance card.
|
|
239
|
+
- The chart covers turns present in the session log fold (whole log), while the pill's total uses
|
|
240
|
+
the shipped `tokenUsage` projection — both are whole-session, but a heavily-compacted session may
|
|
241
|
+
differ by the tokens lost to compaction.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
package/cordis.patch.yml
ADDED