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 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
- # Temporary Holding Version
1
+ # dsh-consumption-badge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -0,0 +1,7 @@
1
+ # dsh-consumption-badge —— bundle patch
2
+ # 本插件作为 profile 的 bundle 层被挂载:profile 的 package.json 把它列进
3
+ # dsh.profile.bundles 后,这一行 insert 就会把它插进配置树。
4
+ # `name:` 必须与安装后的包名完全一致,否则表现为「装上了但界面上没有」。
5
+ - insert:
6
+ - id: consumption-badge
7
+ name: dsh-consumption-badge