dsh-consumption-badge 1.2.1 → 1.3.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.
Files changed (4) hide show
  1. package/README.md +203 -53
  2. package/lib/client.js +893 -159
  3. package/lib/index.js +194 -21
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,61 +2,74 @@
2
2
 
3
3
  A **persistent** DSH (DeepSeek Harness) Web UI plugin that puts a live consumption badge
4
4
  next to every assistant message, and opens a detail panel with the session's spend curve.
5
+ The UI is **bilingual** (follows the shell language, switches live) and amounts can be shown in
6
+ **any of 17 major currencies** — pick one in Settings → Usage.
5
7
 
6
8
  ```
7
- ⛽ 25.6k | ≈¥0.020 | valley
9
+ ⛽ 3.2k | ≈¥0.004 | Off-peak | Balance = ¥42.50
8
10
  ```
9
11
 
10
- - **Collapsed pill** – session-cumulative tokens, estimated cost (always prefixed `≈`), and the
11
- current peak/valley tier (red `peak`, green `valley`), injected into
12
+ - **Collapsed pill** – **that assistant message's own** token count, its **own** estimated cost
13
+ (always prefixed `≈`), its pricing tier (red `Peak`, green `Off-peak`, attributed to the turn's
14
+ request start), and the DeepSeek **account balance**; injected into
12
15
  `conversation.chat.assistant-actions` right after the shipped copy/branch controls.
16
+ The two numbers are **this turn's, not the session's**: the slot renders once per turn
17
+ (`TurnTailNodeView`) and hands the badge that turn's final `messageId`; the host answers with a
18
+ `messageId → message` map (which carries the turn number) next to the per-turn fold, and the badge
19
+ renders that turn's folded total. Mapping is resolved in two steps so the pill is right even
20
+ before the host half is restarted: the host map first, then the `data-turn-tail` attribute the
21
+ shipped conversation view puts on the turn wrapper. If neither resolves (or the turn has no record
22
+ yet) the pill falls back to the session-wide figures instead of showing a false zero, and its
23
+ tooltip names the scope being shown. The balance segment renders `Balance = ¥42.50` when the
24
+ account API answers, `…` while loading and `—` on failure (the reason rides the tooltip); balance
25
+ uses the same 60 s cache as the modal, refreshed once a minute. The modal itself still shows the
26
+ whole session, unchanged.
13
27
  - **Detail modal** (centered, blurred backdrop, `Esc` or backdrop-click to close):
14
28
  cumulative tokens with hit/miss/output split, estimated cost, current pricing tier,
15
29
  DeepSeek account balance, and a **per-turn dual-axis line chart**
16
30
  (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
31
+ input-hit / input-miss / output tokens, and the turn's peak/off-peak flag.
32
+ - **Settings page · Usage** (`settings.section`, id `balance-dsh-consumption`, order 26):
33
+ a top row of four cards — **Total tokens** (with hit/miss/output split, all-history totals),
34
+ **Estimated cost**, **Current window** (peak/off-peak) and **Account balance** (reusing the
35
+ badge's cached balance) — then **all-session history** as one line chart, filterable by time range
36
+ (Today / Last 7 days / Last 30 days / Last 12 months), by workspace
37
+ (All workspaces + every registered workspace + Unassigned), and by an explicit
24
38
  calendar range that overrides the dropdown. Bucket size follows the range:
25
39
  **hourly for today and any single-day calendar range**, daily for multi-day ranges, and
26
40
  monthly for the last 12 months. Left axis (tokens) is hidden until you
27
41
  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
42
+ total, its peak/off-peak split and the models involved; the footer sums the current
43
+ selection. No data in range → a plain "No usage recorded in this range" note instead of an
44
+ empty chart. The cards are **all-history** totals (independent of the filters); the footer is the
31
45
  filtered total, so the two numbers differ by design.
32
46
  - **Personalization integration** (auto-detected, no configuration): when
33
47
  [`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 —
48
+ installed alongside this plugin, a **Usage popup** block is appended to its settings page —
35
49
  same `pp-block` / `pp-field` / `pp-modes` layout and the same `Pill` primitive as the shipped
36
50
  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:
51
+ **Default / Glass / Custom image / Solid color**.
52
+ - **Default** keeps the themed surface; **Glass** is real `backdrop-filter` glass — both the veil
53
+ behind the modal and the panel itself are theme-derived (`--dsw-alias-bg-base`), so light themes
54
+ get a light haze and dark themes a dark one instead of the panel painting a dark slab over the
55
+ page; **Custom image** takes an image URL *or* a local image plus an **Opacity** slider
56
+ (0–100 %, default 85 %) that sets the background layer's own `opacity` — the same semantic as the
57
+ other personalization background rows, so lowering it fades the image over the themed panel
58
+ instead of dimming it; **Solid color** takes a hex colour and derives a light/dark ink palette so
59
+ the cards, chart and tooltips stay readable (the palette also overrides the tooltip surface).
60
+ - A **Glass** switch appears for Custom image and Solid color, so glass composites with them:
47
61
  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.
62
+ translucent over a blurred page. A **Glass strength** slider (0–48 px, default 22 px) sets the
63
+ blur radius of the panel. The overlay behind the modal is deliberately never blurred.
64
+ - **Clear** wipes the image, the URL field and the file picker in one go. The image row shows a
65
+ **thumbnail** plus the live save state (Saving… / Saved on this device / `error: …`), and
66
+ failures are logged with a `[consumption-badge]` tag instead of failing silently. The picker is a
67
+ button (Choose image… / Change image…), not a raw file input, so saving never leaves a
68
+ misleading "no file chosen" label behind.
56
69
  - Local images are **stored on this machine with no size limit**: the picked file is written as a
57
70
  Blob into IndexedDB (`dsh-consumption-assets` → `images`), and the preference only keeps a small
58
71
  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.
72
+ back into a `blob:` URL for the modal; **Clear** deletes the stored record as well.
60
73
  If IndexedDB is unavailable the file is read as a data URL instead, and the row says so.
61
74
  - The choice is a local preference (`dsh-consumption:modal-style`) and is applied to the open
62
75
  modal immediately. Nothing about the conversation or sidebar backgrounds is touched.
@@ -65,11 +78,135 @@ next to every assistant message, and opens a detail panel with the session's spe
65
78
  (24 h cache, 5 s timeout, fails silently). If the installed version is older — and that version
66
79
  was not ignored — a non-blocking card appears in the bottom-right corner with
67
80
  **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.
81
+ and three actions: **Copy update command**, **Ignore this version** (remembered on this machine)
82
+ and **Remind me later** (this page load only). It never blocks the conversation and never covers
83
+ controls.
70
84
 
71
85
  Everything is an **estimate** for reference only — this plugin is not a billing system of record.
72
86
 
87
+ ## Currency
88
+
89
+ The price table is always **CNY per 1M tokens** (that is what DeepSeek bills in); the currency
90
+ setting only decides how amounts are *displayed*. Pick it in **Settings → Usage → Currency**:
91
+
92
+ | Aspect | Behavior |
93
+ | --- | --- |
94
+ | Currencies | CNY (default) plus USD, EUR, GBP, JPY, HKD, TWD, KRW, SGD, AUD, CAD, CHF, INR, RUB, BRL, THB, MXN |
95
+ | What converts | every amount in the plugin: pill (`≈# dsh-consumption-badge
96
+
97
+ A **persistent** DSH (DeepSeek Harness) Web UI plugin that puts a live consumption badge
98
+ next to every assistant message, and opens a detail panel with the session's spend curve.
99
+ The UI is **bilingual** (follows the shell language, switches live) and amounts can be shown in
100
+ **any of 17 major currencies** — pick one in Settings → Usage.
101
+
102
+ ```
103
+ ⛽ 3.2k | ≈¥0.004 | Off-peak | Balance = ¥42.50
104
+ ```
105
+
106
+ - **Collapsed pill** – **that assistant message's own** token count, its **own** estimated cost
107
+ (always prefixed `≈`), its pricing tier (red `Peak`, green `Off-peak`, attributed to the turn's
108
+ request start), and the DeepSeek **account balance**; injected into
109
+ `conversation.chat.assistant-actions` right after the shipped copy/branch controls.
110
+ The two numbers are **this turn's, not the session's**: the slot renders once per turn
111
+ (`TurnTailNodeView`) and hands the badge that turn's final `messageId`; the host answers with a
112
+ `messageId → message` map (which carries the turn number) next to the per-turn fold, and the badge
113
+ renders that turn's folded total. Mapping is resolved in two steps so the pill is right even
114
+ before the host half is restarted: the host map first, then the `data-turn-tail` attribute the
115
+ shipped conversation view puts on the turn wrapper. If neither resolves (or the turn has no record
116
+ yet) the pill falls back to the session-wide figures instead of showing a false zero, and its
117
+ tooltip names the scope being shown. The balance segment renders `Balance = ¥42.50` when the
118
+ account API answers, `…` while loading and `—` on failure (the reason rides the tooltip); balance
119
+ uses the same 60 s cache as the modal, refreshed once a minute. The modal itself still shows the
120
+ whole session, unchanged.
121
+ - **Detail modal** (centered, blurred backdrop, `Esc` or backdrop-click to close):
122
+ cumulative tokens with hit/miss/output split, estimated cost, current pricing tier,
123
+ DeepSeek account balance, and a **per-turn dual-axis line chart**
124
+ (left axis: tokens, right axis: ¥) with a hover tooltip showing the model,
125
+ input-hit / input-miss / output tokens, and the turn's peak/off-peak flag.
126
+ - **Settings page · Usage** (`settings.section`, id `balance-dsh-consumption`, order 26):
127
+ a top row of four cards — **Total tokens** (with hit/miss/output split, all-history totals),
128
+ **Estimated cost**, **Current window** (peak/off-peak) and **Account balance** (reusing the
129
+ badge's cached balance) — then **all-session history** as one line chart, filterable by time range
130
+ (Today / Last 7 days / Last 30 days / Last 12 months), by workspace
131
+ (All workspaces + every registered workspace + Unassigned), and by an explicit
132
+ calendar range that overrides the dropdown. Bucket size follows the range:
133
+ **hourly for today and any single-day calendar range**, daily for multi-day ranges, and
134
+ monthly for the last 12 months. Left axis (tokens) is hidden until you
135
+ toggle it in the legend; the right axis (¥) is always on. Hovering a point shows its
136
+ total, its peak/off-peak split and the models involved; the footer sums the current
137
+ selection. No data in range → a plain "No usage recorded in this range" note instead of an
138
+ empty chart. The cards are **all-history** totals (independent of the filters); the footer is the
139
+ filtered total, so the two numbers differ by design.
140
+ - **Personalization integration** (auto-detected, no configuration): when
141
+ [`dsh-plugin-personalization`](https://www.npmjs.com/package/dsh-plugin-personalization) is
142
+ installed alongside this plugin, a **Usage popup** block is appended to its settings page —
143
+ same `pp-block` / `pp-field` / `pp-modes` layout and the same `Pill` primitive as the shipped
144
+ personalization rows — offering four backgrounds for the consumption modal:
145
+ **Default / Glass / Custom image / Solid color**.
146
+ - **Default** keeps the themed surface; **Glass** is real `backdrop-filter` glass — both the veil
147
+ behind the modal and the panel itself are theme-derived (`--dsw-alias-bg-base`), so light themes
148
+ get a light haze and dark themes a dark one instead of the panel painting a dark slab over the
149
+ page; **Custom image** takes an image URL *or* a local image plus an **Opacity** slider
150
+ (0–100 %, default 85 %) that sets the background layer's own `opacity` — the same semantic as the
151
+ other personalization background rows, so lowering it fades the image over the themed panel
152
+ instead of dimming it; **Solid color** takes a hex colour and derives a light/dark ink palette so
153
+ the cards, chart and tooltips stay readable (the palette also overrides the tooltip surface).
154
+ - A **Glass** switch appears for Custom image and Solid color, so glass composites with them:
155
+ on an image it blurs the image itself (frosted photo), on a solid it makes the colour
156
+ translucent over a blurred page. A **Glass strength** slider (0–48 px, default 22 px) sets the
157
+ blur radius of the panel. The overlay behind the modal is deliberately never blurred.
158
+ - **Clear** wipes the image, the URL field and the file picker in one go. The image row shows a
159
+ **thumbnail** plus the live save state (Saving… / Saved on this device / `error: …`), and
160
+ failures are logged with a `[consumption-badge]` tag instead of failing silently. The picker is a
161
+ button (Choose image… / Change image…), not a raw file input, so saving never leaves a
162
+ misleading "no file chosen" label behind.
163
+ - Local images are **stored on this machine with no size limit**: the picked file is written as a
164
+ Blob into IndexedDB (`dsh-consumption-assets` → `images`), and the preference only keeps a small
165
+ key — so a 20 MB wallpaper no longer blows the `localStorage` quota. On startup the blob is read
166
+ back into a `blob:` URL for the modal; **Clear** deletes the stored record as well.
167
+ If IndexedDB is unavailable the file is read as a data URL instead, and the row says so.
168
+ - The choice is a local preference (`dsh-consumption:modal-style`) and is applied to the open
169
+ modal immediately. Nothing about the conversation or sidebar backgrounds is touched.
170
+ - **Update notice** (same shape as `dsh-plugin-personalization`'s): ~2.5 s after the page loads the
171
+ plugin asks `https://registry.npmjs.org/dsh-consumption-badge/latest` for the newest version
172
+ (24 h cache, 5 s timeout, fails silently). If the installed version is older — and that version
173
+ was not ignored — a non-blocking card appears in the bottom-right corner with
174
+ **current → latest**, the exact `dsh plugin --profile web update dsh-consumption-badge` command,
175
+ and three actions: **Copy update command**, **Ignore this version** (remembered on this machine)
176
+ and **Remind me later** (this page load only). It never blocks the conversation and never covers
177
+ controls.
178
+
179
+ Everything is an **estimate** for reference only — this plugin is not a billing system of record.
180
+
181
+ ), modal cards and both charts' right axis, the settings cards and footer totals, and the account balance (when the balance API reports CNY) |
182
+ | Rates | 1 CNY = n target currency, cached **6 h**. Primary source `open.er-api.com`, fallback `@fawazahmed0/currency-api` via jsDelivr — fetched through the host route `/api/consumption/rates`, and **directly from the browser** when that route is unavailable (both endpoints send `access-control-allow-origin: *`) |
183
+ | Offline | if no live rate can be fetched, a built-in fallback table is used and the settings hint says so |
184
+ | Preference | `localStorage` key `dsh-consumption:currency`; the settings hint shows the active rate, its source and a timestamp |
185
+ | Unchanged | the pricing table, peak/off-peak rules and every stored record stay in CNY — switching currency never rewrites data |
186
+
187
+ ```
188
+ costCNY = (hit/1e6)*hit + (miss/1e6)*miss + (out/1e6)*out [×2 at peak]
189
+ shown = costCNY × rate[currency]
190
+ ```
191
+
192
+ ## Localization
193
+
194
+ The plugin ships **Chinese and English** copy and follows the DSH shell language live — no reload,
195
+ no second settings page.
196
+
197
+ | Aspect | How it works |
198
+ | --- | --- |
199
+ | Source of truth | The client-side `locale` service (`@deepseek-ai/dsh-client-locale`, `ctx.get('locale')`) |
200
+ | Dictionary | `DICT_EN` in `lib/client.js`, keyed by the **Chinese copy itself**; under `zh` no dictionary is needed because the key is the text, and any key missing from `DICT_EN` falls back to that Chinese text (never a blank string) |
201
+ | Live switching | Slot outlets re-render on a locale switch (the shell subscribes to the locale revision), so `tt()` picks up the new language during the next render; the personalization page block renders in its own React root and subscribes to the locale face itself |
202
+ | Slot labels | The action-row label and the settings tab title are **thunks**, resolved by the slot system at read time, so the tab renames itself on a switch |
203
+ | Late service | If the `locale` service is provided after this plugin's `apply()` ran (boot order differs per composition), the first render retries the bind |
204
+ | Missing service | Without the locale service everything falls back to Chinese — the plugin still works standalone |
205
+ | Host errors | `lib/index.js` never returns localized prose: failures carry a language-neutral `code` + `detail` (and an English `error` for logs). The client maps `code` → copy in the active language, and prints the host's `error` only for an unknown code |
206
+
207
+ Adding a language means adding one dictionary next to `DICT_EN`; the lookup order is
208
+ *active locale → key*, so untranslated keys degrade to Chinese rather than breaking.
209
+
73
210
  ## Requirements
74
211
 
75
212
  | Capability | Why |
@@ -125,7 +262,7 @@ deployments where the bundle layer does mount. Keep only **one** of the two acti
125
262
  | Data | Source |
126
263
  | --- | --- |
127
264
  | 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**) |
265
+ | Turn timing | `turn/start` event time (peak/off-peak is attributed to **request start**) |
129
266
  | Model per turn | `request/context` / `request/header.config`; falls back to the agent default model, then `deepseek-flash` |
130
267
  | Session totals | the shipped `tokenUsage` projection when present, otherwise the folded turns |
131
268
  | Balance | `GET https://api.deepseek.com/user/balance` via curl, 60s cache |
@@ -137,7 +274,7 @@ scheduled off the render path (`requestIdleCallback`), and the aggregation is O(
137
274
  The settings page reads **those same records** (no second store, no new field): workspace
138
275
  attribution is resolved at read time from DSH's own accounting ledger
139
276
  (`WorkspaceView.sessionIds` from `useWorkspaces`), so historical records are attributed without
140
- any backfill. Records whose session no longer exists land in the 未归属工作区 bucket.
277
+ any backfill. Records whose session no longer exists land in the **Unassigned** bucket.
141
278
  Above 5000 records the aggregation runs in a Web Worker (built from the pure aggregator's own
142
279
  source, so both paths are the same code); if the browser refuses the Worker it silently falls
143
280
  back to the main thread, where 6000 records aggregate in ~6 ms.
@@ -156,7 +293,7 @@ cost = (hit/1e6)*hit_price + (miss/1e6)*miss_price + (out/1e6)*out_price [×2
156
293
  ```
157
294
 
158
295
  **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**.
296
+ holidays. Everything else, plus weekends, is **off-peak**.
160
297
 
161
298
  Unrecognised model ids are priced as `deepseek-flash` (or `deepseek-v4-pro` when the id contains
162
299
  `pro`) and the UI marks the figure as an estimate. The table lives in `PRICES` at the top of
@@ -172,9 +309,15 @@ The host half registers three exact, read-only routes on the DSH web server:
172
309
 
173
310
  | Route | Returns |
174
311
  | --- | --- |
175
- | `GET /api/consumption/turns?sessionId=<id>` | per-turn `{turn, hit, miss, out, time, model}` (log-length cached) |
312
+ | `GET /api/consumption/turns?sessionId=<id>` | per-turn `{turn, hit, miss, out, time, model}` **plus** per-message `{id, turn, step, hit, miss, out, time, model}` (the `id` is the finalized message id the badge receives as `messageId`); log-length cached |
176
313
  | `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” |
314
+ | `GET /api/consumption/rates[?force=1]` | `{ok, base:"CNY", rates:{USD:…}, source, fetchedAt, stale}` — 16 curated currencies, 6 h cache; `stale:true` means the built-in fallback table |
315
+ | `GET /api/consumption/balance[?force=1]` | `{ok, available, currency, total, granted, toppedUp}`; `is_available:false` renders a red "insufficient balance" |
316
+
317
+ Failures are `{ok:false, code, detail, error}`: `code` is language-neutral
318
+ (`no-credentials`, `no-key`, `key-read-failed`, `no-subprocess`, `curl-empty`, `spawn-failed`,
319
+ `bad-response`, `balance-request-failed`, `missing-session-id`, `log-unavailable`), `detail` is raw
320
+ text, `error` is an English fallback for logs.
178
321
 
179
322
  These routes are **unauthenticated**. If your DSH web server binds `0.0.0.0`, anyone on the
180
323
  network can read the balance and token figures; keep it on `127.0.0.1` unless you need otherwise.
@@ -198,18 +341,20 @@ Install the DSH plugin "dsh-consumption-badge" into my web profile and prove it
198
341
  `{"ok":true,...}`. Verify the client half:
199
342
  `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/plugins/dsh-consumption-badge/client.js`
200
343
  must return `200`.
201
- 5. Open any session and confirm the `⛽ <tokens> | ≈¥<cost> | peak|valley` pill in the assistant
344
+ 5. Open any session and confirm the `⛽ <tokens> | ≈¥<cost> | Peak|Off-peak | Balance = ¥<total>` pill in the assistant
202
345
  action row; click it and confirm the modal shows the four cards, the dual-axis chart, the
203
346
  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`.
347
+ 6. Keep Settings → Usage open, then switch the shell language between Chinese and English
348
+ (Settings → General → Language) and confirm every visible string in the badge, the modal, the
349
+ settings page and the personalization block switches without a reload.
350
+ 7. On the Usage page switch the time range (Today / Last 7 days / Last 30 days / Last 12 months),
351
+ switch the workspace (All workspaces → a specific workspace → Unassigned), set the calendar range
352
+ and confirm both the chart and the footer totals update, and that the Token axis only appears
353
+ after clicking its legend chip.
354
+ 8. If `dsh-plugin-personalization` is installed, open Settings → Personalization and confirm a
355
+ Usage popup block appears at the bottom; switch it to Glass / Solid color / Custom image and
356
+ confirm the ⛽ modal (click a badge) re-renders with that background.
357
+ 9. Report which step failed and paste the boot log lines that mention `consumption-badge`.
213
358
 
214
359
  Rules while doing this: never write to the DSH credentials file yourself (ask me for
215
360
  DEEPSEEK_API_KEY), never bind a second server to the same port, and do not try to persist the
@@ -226,12 +371,16 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
226
371
  `require("react")` — React must stay external (a second React instance breaks slot rendering).
227
372
  - `lib/index.js` imports nothing at all (no `@deepseek-ai/cordis`), so it does not depend on the
228
373
  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.
374
+ - Do not hard-code user-visible English or Chinese in new JSX: call `tt('…')` with the Chinese copy
375
+ as the key and add the English entry to `DICT_EN`. `check.mjs` fails if any `tt()` key has no
376
+ English translation or if a translation still contains Chinese.
377
+ - Self-check without booting DSH: `node check.mjs` (273 assertions over the route contract, the
378
+ client bundle contract, the pricing/aggregation maths and the bilingual copy). It lives outside
379
+ the published `files` list.
231
380
 
232
381
  ## Known limitations
233
382
 
234
- - All amounts are **estimates** — price table, peak/valley rules and model detection can drift
383
+ - All amounts are **estimates** — price table, peak/off-peak rules and model detection can drift
235
384
  from your actual invoice.
236
385
  - One chart point per turn; multiple steps in a turn are summed.
237
386
  - Balance parsing depends on DeepSeek's `/user/balance` response shape; failures only affect the
@@ -239,6 +388,7 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
239
388
  - The chart covers turns present in the session log fold (whole log), while the pill's total uses
240
389
  the shipped `tokenUsage` projection — both are whole-session, but a heavily-compacted session may
241
390
  differ by the tokens lost to compaction.
391
+ - Only `zh` and `en` are shipped; any other shell locale falls back to `zh` (the dictionary keys).
242
392
 
243
393
  ## License
244
394