dsh-consumption-badge 1.3.1 → 1.3.3

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 (3) hide show
  1. package/README.md +55 -6
  2. package/lib/client.js +472 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,8 +2,10 @@
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
+ The UI is **bilingual** (follows the shell language, switches live), costs are estimated for
6
+ **33 model entries across 10 providers**, amounts can be shown in **any of 17 major
7
+ currencies**, and one dropdown picks the **model** you want to look at. All of it lives in
8
+ Settings → Usage.
7
9
 
8
10
  ```
9
11
  ⛽ 3.2k | ≈¥0.004 | Off-peak | Balance = ¥42.50
@@ -26,9 +28,11 @@ The UI is **bilingual** (follows the shell language, switches live) and amounts
26
28
  whole session, unchanged.
27
29
  - **Detail modal** (centered, blurred backdrop, `Esc` or backdrop-click to close):
28
30
  cumulative tokens with hit/miss/output split, estimated cost, current pricing tier,
29
- DeepSeek account balance, and a **per-turn dual-axis line chart**
30
- (left axis: tokens, right axis: ¥) with a hover tooltip showing the model,
31
- input-hit / input-miss / output tokens, and the turn's peak/off-peak flag.
31
+ DeepSeek account balance, a **model dropdown** in the title bar, and a **per-turn dual-axis line
32
+ chart** (left axis: tokens, right axis: money) where **each model gets its own colored line** and
33
+ the legend names them; the hover tooltip shows the model (with its color dot), input-hit /
34
+ input-miss / output tokens, the turn's cost and its peak/off-peak flag. Picking a model filters the
35
+ cards + chart to that model and turns the balance hint into "about N tokens left at these prices".
32
36
  - **Settings page · Usage** (`settings.section`, id `balance-dsh-consumption`, order 26):
33
37
  a top row of four cards — **Total tokens** (with hit/miss/output split, all-history totals),
34
38
  **Estimated cost**, **Current window** (peak/off-peak) and **Account balance** (reusing the
@@ -84,6 +88,49 @@ The UI is **bilingual** (follows the shell language, switches live) and amounts
84
88
 
85
89
  Everything is an **estimate** for reference only — this plugin is not a billing system of record.
86
90
 
91
+ ## Models and pricing
92
+
93
+ Costs are estimated from a **flat price table of 31 model entries** (current generations first,
94
+ legacy entries last so old logs still resolve). Unknown ids fall back to a family guess
95
+ (`pro`/`opus`/`max` → the stronger tier, everything else → DeepSeek V3) and are marked as estimates.
96
+
97
+ | Provider | Current | Legacy (kept for old logs) |
98
+ | --- | --- | --- |
99
+ | DeepSeek | `deepseek-flash` = **DeepSeek-V4.1-Flash-1017**, `deepseek-v4-pro` = **DeepSeek-V4-Pro-0813** (official ids/versions) | DeepSeek V3.2 |
100
+ | OpenAI | GPT-5.6 Sol, GPT-5.6 Terra, GPT-5.6 Luna, o3 | GPT-4o, GPT-4o mini |
101
+ | Anthropic | Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 | Claude Sonnet 4.6, Opus 4.x |
102
+ | Google | Gemini 3.8 Flash, Gemini 3.1 Pro, Gemini 3 Flash | Gemini 2.5 Flash |
103
+ | Alibaba | Qwen3.8 Max, Qwen3.8 Flash | Qwen3.7 Plus |
104
+ | Zhipu | GLM-5.3, GLM-5.1 | GLM-4.6 |
105
+ | Moonshot | Kimi K3.1 Pro, Kimi K3 | Kimi K2.5 |
106
+ | xAI | Grok 4.7 | Grok 4.3 |
107
+ | Meta | Llama 4 Maverick | — |
108
+ | Mistral | Mistral Large 4 | — |
109
+
110
+ Rules that follow from the table:
111
+
112
+ - **Every turn is priced with the model it actually used** — a session that mixed models is not
113
+ priced with a single rate. Sessions, buckets and charts all keep the per-model split.
114
+ - **Only DeepSeek has peak/off-peak pricing** (×2 in the Beijing-time peak windows). Other vendors
115
+ are flat; their card hint says so.
116
+ - **Prices are CNY per 1M tokens.** Non-DeepSeek entries are the vendor's USD list price × 7.1
117
+ (input-cache-read / input / output), taken from OpenRouter's public model catalogue; the two
118
+ harness-native DeepSeek rows keep DeepSeek's official CNY table. Edit `MODELS` at the top of
119
+ `lib/client.js` — each entry carries `price {hit, miss, out}`, `peakPricing`, `legacy` and a chart
120
+ `color`.
121
+ - **Model ids are resolved transparently.** `deepseek-flash` is the id DeepSeek's API currently
122
+ routes to **DeepSeek-V4.1-Flash-1017** and `deepseek-v4-pro` to **DeepSeek-V4-Pro-0813** (per
123
+ `api-docs.deepseek.com`), and deprecated ids (`deepseek-v4-flash*`, `deepseek-chat`) route to the
124
+ same families. Every model label in the UI can therefore be checked against the log: the settings
125
+ cost card, the modal cost card, the legend tooltip and the per-turn tooltip all print
126
+ `Family name (Official MODEL VERSION · log id <id>)`, so a mismatch is visible instead of hidden.
127
+ - The **Model** dropdown (panel title bar *and* Settings → Usage, sharing one preference stored in
128
+ `localStorage: dsh-consumption:model-filter`) lists models **grouped by provider** in
129
+ `<optgroup>`s; it filters the statistics **and** acts as the pricing fallback for turns whose log
130
+ entry carries no model. `All models` turns both behaviours off.
131
+ - Charts draw **one line per model** in that model's color (33 distinct colors), with a legend of
132
+ model chips; the token axis stays a single dashed neutral line.
133
+
87
134
  ## Currency
88
135
 
89
136
  The price table is always **CNY per 1M tokens** (that is what DeepSeek bills in); the currency
@@ -202,6 +249,8 @@ no second settings page.
202
249
  | 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
250
  | 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
251
  | Missing service | Without the locale service everything falls back to Chinese — the plugin still works standalone |
252
+ | Both dictionaries | The plugin registers **both** `zh` (identity: key→key) and `en`, so the Chinese result never depends on how a particular host build resolves a missing locale (some use `en` as the fallback locale, which would otherwise leak English into a Chinese UI) |
253
+ | Language override | Settings → Usage → **Language**: `Follow shell` / `Chinese` / `English`. It overrides the shell for this plugin only, persists in `localStorage: dsh-consumption:language`, and is the escape hatch when a shell reports an unexpected locale |
205
254
  | 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
255
 
207
256
  Adding a language means adding one dictionary next to `DICT_EN`; the lookup order is
@@ -374,7 +423,7 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
374
423
  - Do not hard-code user-visible English or Chinese in new JSX: call `tt('…')` with the Chinese copy
375
424
  as the key and add the English entry to `DICT_EN`. `check.mjs` fails if any `tt()` key has no
376
425
  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
426
+ - Self-check without booting DSH: `node check.mjs` (334 assertions over the route contract, the
378
427
  client bundle contract, the pricing/aggregation maths and the bilingual copy). It lives outside
379
428
  the published `files` list.
380
429
 
package/lib/client.js CHANGED
@@ -20,9 +20,63 @@ window.__ModuleLoader__.load({
20
20
 
21
21
  const API = '/api/consumption'
22
22
  const CSS_ID = 'dsh-consumption-badge'
23
- const PRICES = {
24
- 'deepseek-flash': { hit: 0.02, miss: 1, out: 4 },
25
- 'deepseek-v4-pro': { hit: 0.15, miss: 4.5, out: 13.5 },
23
+
24
+ /**
25
+ * 主流大模型价目表(**统一折算成人民币 / 1M tokens**,仅供估算)。
26
+ * - `hit` = 输入命中缓存、`miss` = 输入未命中、`out` = 输出;
27
+ * - 生成顺序:每家厂商**当前版本在前、legacy 在后**;`legacy: true` 只用于承接历史日志里的旧 id;
28
+ * - 非人民币原生价一律按官方 **USD 价 × 7.1** 折算(表里已经是折好的人民币,改价直接改这张表);
29
+ * - `peakPricing: true` 的模型才有 DeepSeek 那种「峰时 ×2」(其它厂商不区分峰谷);
30
+ * - `match` 是大小写无关的子串匹配,并且**最长命中优先**,所以 `gpt-5.6-terra-pro`、
31
+ * `claude-sonnet-4-5`、`deepseek-v4.1-flash-1017` 这类带后缀/日期的 id 都能落到正确家族。
32
+ *
33
+ * 当前版本(2026-11 口径):DeepSeek V4.1 Flash / V4 Pro(0813) / V3.2、GPT-5.6 Sol/Terra/Luna、
34
+ * Claude Opus·Sonnet·Haiku 5.5、Gemini 3.8 Flash / 3.1 Pro / 3 Flash、Qwen3.8 Max/Flash、
35
+ * GLM-5.3 / 5.1、Kimi K3.1 Pro / K3、Grok 4.7、Llama 4 Maverick、Mistral Large 4。
36
+ */
37
+ const MODELS = [
38
+ // 官方:MODEL deepseek-flash → MODEL VERSION DeepSeek-V4.1-Flash-1017(旧 id deepseek-v4-flash / -vision-exp 仍可调用并按 Flash 计费)
39
+ { id: 'deepseek-flash', provider: 'DeepSeek', label: 'DeepSeek V4.1 Flash', version: 'DeepSeek-V4.1-Flash-1017', legacy: false, match: ['deepseek-v4.1-flash', 'deepseek-v4-flash', 'deepseek-flash', 'deepseek-chat'], price: { hit: 0.02, miss: 1, out: 4 }, peakPricing: true, color: '#4d6bfe' },
40
+ // 官方:MODEL deepseek-v4-pro → MODEL VERSION DeepSeek-V4-Pro-0813
41
+ { id: 'deepseek-v4-pro', provider: 'DeepSeek', label: 'DeepSeek V4 Pro', version: 'DeepSeek-V4-Pro-0813', legacy: false, match: ['deepseek-v4-pro-0813', 'deepseek-v4-pro-0423', 'deepseek-v4-pro', 'deepseek-reasoner', 'deepseek-r1'], price: { hit: 0.15, miss: 4.5, out: 13.5 }, peakPricing: true, color: '#8b5cf6' },
42
+ // 老一代:只在历史日志里出现,按第三方零售价折算,仅作兜底估算
43
+ { id: 'deepseek-v3.2', provider: 'DeepSeek', label: 'DeepSeek V3.2 (legacy)', version: 'DeepSeek-V3.2', legacy: true, match: ['deepseek-v3.2', 'deepseek-v3'], price: { hit: 0.923, miss: 1.846, out: 2.698 }, peakPricing: true, color: '#818cf8' },
44
+ { id: 'gpt-5.6-sol', provider: 'OpenAI', label: 'GPT-5.6 Sol', legacy: false, match: ['gpt-5.6-sol'], price: { hit: 2.84, miss: 28.4, out: 142 }, peakPricing: false, color: '#0f766e' },
45
+ { id: 'gpt-5.6-terra', provider: 'OpenAI', label: 'GPT-5.6 Terra', legacy: false, match: ['gpt-5.6-terra'], price: { hit: 1.42, miss: 14.2, out: 85.2 }, peakPricing: false, color: '#10a37f' },
46
+ { id: 'gpt-5.6-luna', provider: 'OpenAI', label: 'GPT-5.6 Luna', legacy: false, match: ['gpt-5.6-luna'], price: { hit: 0.142, miss: 1.42, out: 8.52 }, peakPricing: false, color: '#34d399' },
47
+ { id: 'o3', provider: 'OpenAI', label: 'o3 / o3-pro', legacy: false, match: ['o3', 'o1', 'o4-mini'], price: { hit: 3.55, miss: 14.2, out: 56.8 }, peakPricing: false, color: '#059669' },
48
+ { id: 'gpt-4o', provider: 'OpenAI', label: 'GPT-4o (legacy)', legacy: true, match: ['gpt-4o'], price: { hit: 8.88, miss: 17.75, out: 71 }, peakPricing: false, color: '#22c55e' },
49
+ { id: 'gpt-4o-mini', provider: 'OpenAI', label: 'GPT-4o mini (legacy)', legacy: true, match: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4-mini'], price: { hit: 0.53, miss: 1.07, out: 4.26 }, peakPricing: false, color: '#86efac' },
50
+ { id: 'claude-opus-5.5', provider: 'Anthropic', label: 'Claude Opus 5.5', legacy: false, match: ['claude-opus-5', 'claude-opus'], price: { hit: 1.42, miss: 28.4, out: 142 }, peakPricing: false, color: '#c2410c' },
51
+ { id: 'claude-sonnet-5.5', provider: 'Anthropic', label: 'Claude Sonnet 5.5', legacy: false, match: ['claude-sonnet-5', 'claude-sonnet'], price: { hit: 0.71, miss: 14.2, out: 71 }, peakPricing: false, color: '#d97757' },
52
+ { id: 'claude-haiku-5.5', provider: 'Anthropic', label: 'Claude Haiku 5.5', legacy: false, match: ['claude-haiku-5', 'claude-haiku'], price: { hit: 0.071, miss: 0.71, out: 3.55 }, peakPricing: false, color: '#fb923c' },
53
+ { id: 'claude-sonnet-4.6', provider: 'Anthropic', label: 'Claude Sonnet 4.6 (legacy)', legacy: true, match: ['claude-sonnet-4'], price: { hit: 2.13, miss: 21.3, out: 106.5 }, peakPricing: false, color: '#f59e0b' },
54
+ { id: 'claude-opus-4', provider: 'Anthropic', label: 'Claude Opus 4.x (legacy)', legacy: true, match: ['claude-opus-4', 'opus-4', 'opus-3'], price: { hit: 3.55, miss: 35.5, out: 177.5 }, peakPricing: false, color: '#b45309' },
55
+ { id: 'gemini-3.8-flash', provider: 'Google', label: 'Gemini 3.8 Flash', legacy: false, match: ['gemini-3.8-flash', 'gemini-3.7-flash', 'gemini-3.6-flash'], price: { hit: 0.5858, miss: 5.8575, out: 29.2875 }, peakPricing: false, color: '#0ea5e9' },
56
+ { id: 'gemini-3.1-pro', provider: 'Google', label: 'Gemini 3.1 Pro', legacy: false, match: ['gemini-3.1-pro', 'gemini-3-pro', 'gemini-pro'], price: { hit: 1.42, miss: 14.2, out: 85.2 }, peakPricing: false, color: '#4285f4' },
57
+ { id: 'gemini-3-flash', provider: 'Google', label: 'Gemini 3 Flash', legacy: false, match: ['gemini-3-flash', 'gemini-flash'], price: { hit: 0.355, miss: 3.55, out: 21.3 }, peakPricing: false, color: '#38bdf8' },
58
+ { id: 'gemini-2.5-flash', provider: 'Google', label: 'Gemini 2.5 Flash (legacy)', legacy: true, match: ['gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5'], price: { hit: 0.213, miss: 2.13, out: 17.75 }, peakPricing: false, color: '#7dd3fc' },
59
+ { id: 'qwen3.8-max', provider: 'Alibaba', label: 'Qwen3.8 Max', legacy: false, match: ['qwen3.8-max', 'qwen3-max', 'qwen-max', 'qwen-max-latest'], price: { hit: 1.775, miss: 14.2, out: 42.6 }, peakPricing: false, color: '#615ced' },
60
+ { id: 'qwen3.8-flash', provider: 'Alibaba', label: 'Qwen3.8 Flash', legacy: false, match: ['qwen3.8-flash', 'qwen3.7-flash', 'qwen-flash'], price: { hit: 0.1136, miss: 1.065, out: 3.337 }, peakPricing: false, color: '#a78bfa' },
61
+ { id: 'qwen3.7-plus', provider: 'Alibaba', label: 'Qwen3.7 Plus (legacy)', legacy: true, match: ['qwen3.7-plus', 'qwen-plus', 'qwen-turbo', 'qwen3'], price: { hit: 0.4544, miss: 2.272, out: 9.088 }, peakPricing: false, color: '#c4b5fd' },
62
+ { id: 'glm-5.3', provider: 'Zhipu', label: 'GLM-5.3', legacy: false, match: ['glm-5.3', 'glm-5'], price: { hit: 0.5396, miss: 0.568, out: 25.56 }, peakPricing: false, color: '#0d9488' },
63
+ { id: 'glm-5.1', provider: 'Zhipu', label: 'GLM-5.1', legacy: false, match: ['glm-5.1'], price: { hit: 1.846, miss: 9.94, out: 31.24 }, peakPricing: false, color: '#2dd4bf' },
64
+ { id: 'glm-4.6', provider: 'Zhipu', label: 'GLM-4.6 (legacy)', legacy: true, match: ['glm-4.7', 'glm-4.6', 'glm-4-plus', 'glm-4.5', 'glm-4-air'], price: { hit: 0.71, miss: 3.55, out: 14.2 }, peakPricing: false, color: '#99f6e4' },
65
+ { id: 'kimi-k3.1-pro', provider: 'Moonshot', label: 'Kimi K3.1 Pro', legacy: false, match: ['kimi-k3.1-pro'], price: { hit: 1.42, miss: 14.2, out: 71 }, peakPricing: false, color: '#db2777' },
66
+ { id: 'kimi-k3', provider: 'Moonshot', label: 'Kimi K3', legacy: false, match: ['kimi-k3', 'kimi'], price: { hit: 1.2922, miss: 1.3632, out: 106.5 }, peakPricing: false, color: '#f472b6' },
67
+ { id: 'kimi-k2.5', provider: 'Moonshot', label: 'Kimi K2.5 (legacy)', legacy: true, match: ['kimi-k2', 'moonshot-v1'], price: { hit: 1.065, miss: 3.55, out: 17.75 }, peakPricing: false, color: '#fbcfe8' },
68
+ { id: 'grok-4.7', provider: 'xAI', label: 'Grok 4.7', legacy: false, match: ['grok-4.7', 'grok-4.6', 'grok-4.5', 'grok-4'], price: { hit: 3.55, miss: 14.2, out: 42.6 }, peakPricing: false, color: '#64748b' },
69
+ { id: 'grok-4.3', provider: 'xAI', label: 'Grok 4.3 (legacy)', legacy: true, match: ['grok-4.3', 'grok-4.20', 'grok-3', 'grok-2'], price: { hit: 1.42, miss: 8.875, out: 17.75 }, peakPricing: false, color: '#94a3b8' },
70
+ { id: 'llama-4-maverick', provider: 'Meta', label: 'Llama 4 Maverick', legacy: false, match: ['llama-4', 'llama-3', 'llama3'], price: { hit: 0.355, miss: 1.3312, out: 4.6328 }, peakPricing: false, color: '#06b6d4' },
71
+ { id: 'mistral-large-4', provider: 'Mistral', label: 'Mistral Large 4', legacy: false, match: ['mistral-large-4', 'mistral-large', 'mistral-medium', 'mistral'], price: { hit: 0.497, miss: 4.97, out: 14.91 }, peakPricing: false, color: '#f97316' },
72
+ ]
73
+ /** 兼容旧版本:按 id 建索引;`PRICES[id]` 额外带 `peak` 标记,Worker 里自包含的聚合函数直接读它。 */
74
+ const PRICES = {}
75
+ const MODEL_BY_ID = {}
76
+ for (let mi = 0; mi < MODELS.length; mi += 1) {
77
+ const entry = MODELS[mi]
78
+ PRICES[entry.id] = { hit: entry.price.hit, miss: entry.price.miss, out: entry.price.out, peak: entry.peakPricing === true }
79
+ MODEL_BY_ID[entry.id] = entry
26
80
  }
27
81
  const DEFAULT_MODEL = 'deepseek-flash'
28
82
  const PEAK_MULTIPLIER = 2
@@ -65,18 +119,82 @@ window.__ModuleLoader__.load({
65
119
  }
66
120
  return false
67
121
  }
68
- // Unknown model ids still price as flash (or pro for pro-like ids) and are flagged.
69
- function priceFor(model) {
70
- const id = typeof model === 'string' ? model : ''
71
- if (PRICES[id]) return { id: id, table: PRICES[id], known: true }
72
- if (id.toLowerCase().indexOf('pro') >= 0) return { id: id, table: PRICES['deepseek-v4-pro'], known: false }
73
- return { id: id || DEFAULT_MODEL, table: PRICES[DEFAULT_MODEL], known: false }
122
+ /**
123
+ * 把任意模型 id 归到价目表里的一个家族。
124
+ * 先精确命中 id,再按 `match` 子串大小写无关匹配;都不中就用兜底模型(并标 `known:false`,
125
+ * UI 会把金额标成估算)。`pro`/`large`/`opus` 这类更强的名字在没有精确信息时会落到更贵的一档。
126
+ */
127
+ function resolveModel(model, fallbackId) {
128
+ const raw = typeof model === 'string' ? model.trim() : ''
129
+ const lower = raw.toLowerCase()
130
+ const fallback = typeof fallbackId === 'string' && MODEL_BY_ID[fallbackId] ? MODEL_BY_ID[fallbackId] : MODEL_BY_ID[DEFAULT_MODEL]
131
+ if (raw !== '') {
132
+ if (MODEL_BY_ID[raw]) return { id: raw, entry: MODEL_BY_ID[raw], known: true, raw: raw }
133
+ // 取**最长命中**的家族:`gpt-4o-mini-2026` 必须落到 gpt-4o-mini,而不是被 gpt-4o 抢走。
134
+ let best = null
135
+ let bestLen = 0
136
+ for (let i = 0; i < MODELS.length; i += 1) {
137
+ const entry = MODELS[i]
138
+ for (let k = 0; k < entry.match.length; k += 1) {
139
+ const needle = entry.match[k]
140
+ if (needle.length > bestLen && lower.indexOf(needle) >= 0) {
141
+ best = entry
142
+ bestLen = needle.length
143
+ }
144
+ }
145
+ }
146
+ if (best !== null) return { id: best.id, entry: best, known: true, raw: raw }
147
+ if (lower.indexOf('pro') >= 0 || lower.indexOf('opus') >= 0 || lower.indexOf('max') >= 0) {
148
+ return { id: 'deepseek-v4-pro', entry: MODEL_BY_ID['deepseek-v4-pro'], known: false, raw: raw }
149
+ }
150
+ }
151
+ return { id: fallback.id, entry: fallback, known: raw === '', raw: raw }
74
152
  }
153
+ /**
154
+ * 模型的可解释文案:`DeepSeek V4.1 Flash(DeepSeek-V4.1-Flash-1017 · 日志 id deepseek-flash)`
155
+ * 先显示价目表族名,再显示官方 MODEL VERSION 和日志里真实的 id,避免「明明用 V4.1 却显示 V3」这种误会。
156
+ */
157
+ function modelDetail(model) {
158
+ const info = priceFor(model)
159
+ const bits = []
160
+ if (info.version) bits.push(info.version)
161
+ const raw = String(info.raw || '')
162
+ if (raw && raw !== info.id) bits.push(tt('日志 id {id}', { id: raw }))
163
+ else if (raw) bits.push(tt('日志 id {id}', { id: raw }))
164
+ return bits.length > 0 ? info.label + '(' + bits.join(' · ') + ')' : info.label
165
+ }
166
+ function modelColor(model) {
167
+ return resolveModel(model).entry.color
168
+ }
169
+ function modelLabel(model) {
170
+ const r = resolveModel(model)
171
+ return r.known || r.raw === '' ? r.entry.label : r.raw
172
+ }
173
+ /** 计价入口:返回价目表条目(含 `peak` 标记)+ 该模型的展示信息。 */
174
+ function priceFor(model, fallbackId) {
175
+ const r = resolveModel(model, fallbackId)
176
+ return {
177
+ id: r.id,
178
+ table: PRICES[r.id],
179
+ known: r.known,
180
+ peakPricing: r.entry.peakPricing === true,
181
+ label: r.entry.label,
182
+ version: r.entry.version || '',
183
+ provider: r.entry.provider,
184
+ color: r.entry.color,
185
+ raw: r.raw,
186
+ }
187
+ }
188
+ /** 按模型计价:只有标了 `peak` 的模型(DeepSeek)才乘峰时系数,其它厂商不区分峰谷。 */
75
189
  function costOf(bucket, table, peak) {
76
- const k = peak ? PEAK_MULTIPLIER : 1
190
+ const k = peak && table && table.peak === true ? PEAK_MULTIPLIER : 1
77
191
  const cost = (bucket.hit / 1e6) * table.hit + (bucket.miss / 1e6) * table.miss + (bucket.out / 1e6) * table.out
78
192
  return cost * k
79
193
  }
194
+ /** 按「记录自己的模型」计价(多模型混跑时逐条算,不混价)。 */
195
+ function costOfModel(bucket, model, peak, fallbackId) {
196
+ return costOf(bucket, priceFor(model, fallbackId).table, peak)
197
+ }
80
198
  function fmtTokens(v) {
81
199
  const n = Math.max(0, Math.round(Number(v) || 0))
82
200
  if (n < 1000) return String(n)
@@ -146,6 +264,12 @@ window.__ModuleLoader__.load({
146
264
  const raw = lsGet(LS_CURRENCY)
147
265
  return currencyMeta(raw) !== null ? raw : 'CNY'
148
266
  }
267
+ const LS_MODEL_FILTER = 'dsh-consumption:model-filter'
268
+ /** 当前「模型」下拉的选择('' = 全部模型)。非法值一律当全部。 */
269
+ function readModelFilter() {
270
+ const raw = lsGet(LS_MODEL_FILTER)
271
+ return typeof raw === 'string' && MODEL_BY_ID[raw] ? raw : ''
272
+ }
149
273
  /**
150
274
  * 把人民币金额换算成当前偏好币种。
151
275
  * @returns {{value: number, code: string, rate: number, converted: boolean}}
@@ -241,6 +365,25 @@ window.__ModuleLoader__.load({
241
365
  }
242
366
  return chain
243
367
  }
368
+ /** 把模型列表渲染成按厂商分组的 <optgroup>(设置页与弹层共用)。 */
369
+ function modelOptionNodes(list) {
370
+ const order = []
371
+ const byProvider = {}
372
+ for (let i = 0; i < list.length; i += 1) {
373
+ const m = list[i]
374
+ if (!byProvider[m.provider]) {
375
+ byProvider[m.provider] = []
376
+ order.push(m.provider)
377
+ }
378
+ byProvider[m.provider].push(m)
379
+ }
380
+ return order.map(function (provider) {
381
+ return h('optgroup', { key: provider, label: provider },
382
+ byProvider[provider].map(function (m) {
383
+ return h('option', { key: m.id, value: m.id }, m.label)
384
+ }))
385
+ })
386
+ }
244
387
  function bucketOfTurns(turns) {
245
388
  const out = { hit: 0, miss: 0, out: 0 }
246
389
  for (let i = 0; i < turns.length; i += 1) {
@@ -286,6 +429,16 @@ window.__ModuleLoader__.load({
286
429
  '命中 {hit} · 未命中 {miss} · 输出 {out}': 'hit {hit} · miss {miss} · output {out}',
287
430
  '本轮消耗(点击查看本会话明细)': 'This turn’s usage (click for session details)',
288
431
  '货币': 'Currency',
432
+ '模型': 'Model',
433
+ '全部模型': 'All models',
434
+ '按模型筛选;同时作为计价兜底': 'Filter by model; also the pricing fallback',
435
+ '仅 {model}': 'only {model}',
436
+ '日志 id {id}': 'log id {id}',
437
+ '语言': 'Language',
438
+ '跟随外壳': 'Follow shell',
439
+ '{count} 个模型各自计价': '{count} models priced separately',
440
+ '按 {model} 约还可跑 {tokens} tokens': 'about {tokens} tokens left at {model} prices',
441
+ '金额按各模型官方价估算(DeepSeek 区分峰谷 ×1/×2,其它厂商按 USD 价 ×7.1 折人民币),仅供参考;数据源为本会话日志,刷新后自动恢复。': 'Costs are estimated from each model’s list price (DeepSeek applies peak/off-peak ×1/×2; other vendors are converted from USD at 7.1), for reference only; the source is this session log, restored automatically after a refresh.',
289
442
  '金额按人民币报价折算显示': 'Amounts are priced in CNY and converted for display',
290
443
  '当前以人民币显示(可切换成其它币种)': 'Showing CNY — pick another currency to convert',
291
444
  '1 CNY = {rate} {code}': '1 CNY = {rate} {code}',
@@ -426,6 +579,30 @@ window.__ModuleLoader__.load({
426
579
  let boundTranslate = null
427
580
  let localeService = null
428
581
  let appCtx = null
582
+ // 插件自带的语言覆盖:'' = 跟随外壳,'zh' / 'en' = 用户显式指定。
583
+ // 存在的理由:外壳的 locale 服务在不同版本里回退策略不同(本版 FALLBACK_LOCALE=zh,
584
+ // 换成 en 的实现就会让中文界面吃 en 词典而显示英文),npm 包又装在别人的环境里,
585
+ // 所以这里既**显式注册 zh 词典**,也允许用户自己钉死语言。
586
+ const LS_LANGUAGE = 'dsh-consumption:language'
587
+ let languageOverride = readLanguage()
588
+
589
+ function readLanguage() {
590
+ const raw = lsGet(LS_LANGUAGE)
591
+ return raw === 'zh' || raw === 'en' ? raw : ''
592
+ }
593
+ function setLanguageOverride(next) {
594
+ const value = next === 'zh' || next === 'en' ? next : ''
595
+ languageOverride = value
596
+ lsSet(LS_LANGUAGE, value)
597
+ }
598
+ /** 语言下拉的选项(跟随外壳 / 中文 / English)。 */
599
+ function languageOptions() {
600
+ return [
601
+ { id: '', label: tt('跟随外壳') },
602
+ { id: 'zh', label: '中文' },
603
+ { id: 'en', label: 'English' },
604
+ ]
605
+ }
429
606
 
430
607
  /** Fill `{name}` placeholders (same template syntax as the locale service). */
431
608
  function applyParams(template, params) {
@@ -464,14 +641,20 @@ window.__ModuleLoader__.load({
464
641
  if (service === null || typeof service.register !== 'function' || typeof service.bind !== 'function') return
465
642
  // Only the `en` side is registered: the key already is the `zh` copy, and
466
643
  // the locale service resolves a miss to the key itself.
644
+ // 中英两侧都显式注册:zh 侧是恒等映射(key 就是中文原文),
645
+ // 这样中文界面的结果不再取决于宿主 locale 实现的回退策略(有的版本回退到 en)。
646
+ const dictZh = {}
647
+ for (const key in DICT_EN) {
648
+ if (Object.prototype.hasOwnProperty.call(DICT_EN, key)) dictZh[key] = key
649
+ }
467
650
  let ns = LOCALE_NS
468
651
  let dispose = null
469
652
  try {
470
- dispose = service.register(ns, 'en', DICT_EN)
653
+ dispose = service.register(ns, { zh: dictZh, en: DICT_EN })
471
654
  } catch (e) {
472
655
  ns = LOCALE_NS + '#' + BUILD_TAG + '-' + Math.random().toString(36).slice(2, 7)
473
656
  try {
474
- dispose = service.register(ns, 'en', DICT_EN)
657
+ dispose = service.register(ns, { zh: dictZh, en: DICT_EN })
475
658
  console.warn('[consumption-badge] previous dictionary still registered; bound a private locale namespace ' + ns)
476
659
  } catch (e2) {
477
660
  console.warn('[consumption-badge] locale dictionary registration failed: ' + msgOf(e2))
@@ -497,8 +680,16 @@ window.__ModuleLoader__.load({
497
680
  attachLocale(appCtx)
498
681
  }
499
682
 
500
- /** Translate a key with the active locale; falls back to the key itself. */
683
+ /**
684
+ * 取当前语言的文案。优先级:用户显式选择 → 外壳 locale 服务 → 原文(中文)。
685
+ * 显式选中文时直接返回 key(中文就是 key 本身),完全不经过宿主,避免任何回退策略问题。
686
+ */
501
687
  function tt(key, params) {
688
+ if (languageOverride === 'zh') return applyParams(key, params)
689
+ if (languageOverride === 'en') {
690
+ const hit = Object.prototype.hasOwnProperty.call(DICT_EN, key) ? DICT_EN[key] : key
691
+ return applyParams(hit, params)
692
+ }
502
693
  if (localeService === null) ensureLocaleBound()
503
694
  if (boundTranslate !== null) {
504
695
  try {
@@ -520,8 +711,9 @@ window.__ModuleLoader__.load({
520
711
  }
521
712
  }
522
713
 
523
- /** Active locale id ('zh' when the service is absent). */
714
+ /** Active locale id: 覆盖优先,其次外壳,最后按中文。 */
524
715
  function activeLocaleId() {
716
+ if (languageOverride !== '') return languageOverride
525
717
  try {
526
718
  const snapshot = localeService !== null && typeof localeService.getLocale === 'function' ? localeService.getLocale() : null
527
719
  if (snapshot && typeof snapshot.active === 'string' && snapshot.active) return snapshot.active
@@ -648,10 +840,10 @@ window.__ModuleLoader__.load({
648
840
 
649
841
  /* ================= 消耗弹窗背景样式(与个性化插件联动) ================= */
650
842
  const MODAL_STYLE_KEY = 'dsh-consumption:modal-style'
651
- const BUILD_TAG = 'dcb-1.3.1'
843
+ const BUILD_TAG = 'dcb-1.3.3'
652
844
  // 本地版本:本包是零构建的手写客户端包,无法 import package.json,因此把版本号作为常量维护,
653
845
  // 并由 check.mjs 守卫与 package.json 严格一致(发布前不一致直接失败)。
654
- const PLUGIN_VERSION = '1.3.1'
846
+ const PLUGIN_VERSION = '1.3.3'
655
847
  const PKG_NAME = 'dsh-consumption-badge'
656
848
  const UPDATE_REGISTRY_URL = 'https://registry.npmjs.org/' + PKG_NAME + '/latest'
657
849
  const UPDATE_COMMAND = 'dsh plugin --profile web update ' + PKG_NAME
@@ -1042,6 +1234,10 @@ window.__ModuleLoader__.load({
1042
1234
  modalStyle: readModalStyle(),
1043
1235
  // 显示币种(本地偏好)+ 汇率(Host 路由,6h 缓存):金额展示都走 money()。
1044
1236
  currency: readCurrency(),
1237
+ // 模型筛选 / 计价兜底('' = 全部模型,不筛选;同时作为查不到模型时的计价基准)。
1238
+ modelFilter: readModelFilter(),
1239
+ // 语言覆盖('' = 跟随外壳;'zh' / 'en' = 用户显式指定)。
1240
+ language: readLanguage(),
1045
1241
  rates: null,
1046
1242
  ratesState: 'idle',
1047
1243
  ratesError: '',
@@ -1295,11 +1491,30 @@ window.__ModuleLoader__.load({
1295
1491
  emit()
1296
1492
  if (next !== 'CNY') ensureRates(false)
1297
1493
  }
1494
+ /** 切换界面语言:'' 跟随外壳,或钉死 zh / en(落 localStorage,立即重绘)。 */
1495
+ function setLanguage(next) {
1496
+ const value = next === 'zh' || next === 'en' ? next : ''
1497
+ if (state.language === value) return
1498
+ setLanguageOverride(value)
1499
+ state.language = value
1500
+ emit()
1501
+ }
1298
1502
  /** 强制刷新汇率(设置页的「刷新汇率」按钮)。 */
1299
1503
  function refreshRates() {
1300
1504
  state.rates = null
1301
1505
  return ensureRates(true)
1302
1506
  }
1507
+ /**
1508
+ * 切换「模型」下拉:'' = 全部模型(不筛选);
1509
+ * 选了具体模型时,它同时是**统计筛选条件**和**计价兜底模型**。
1510
+ */
1511
+ function setModelFilter(id) {
1512
+ const next = typeof id === 'string' && MODEL_BY_ID[id] ? id : ''
1513
+ if (state.modelFilter === next) return
1514
+ state.modelFilter = next
1515
+ lsSet(LS_MODEL_FILTER, next)
1516
+ emit()
1517
+ }
1303
1518
  /** 更新弹窗背景配置:合并 → 落本地偏好 → 通知所有订阅者(弹层即时重绘)。 */
1304
1519
  function setModalStyle(patch) {
1305
1520
  const prev = state.modalStyle
@@ -1462,6 +1677,8 @@ window.__ModuleLoader__.load({
1462
1677
  ensureRates: ensureRates,
1463
1678
  setCurrency: setCurrency,
1464
1679
  refreshRates: refreshRates,
1680
+ setModelFilter: setModelFilter,
1681
+ setLanguage: setLanguage,
1465
1682
  openPanel: openPanel,
1466
1683
  closePanel: closePanel,
1467
1684
  setModalStyle: setModalStyle,
@@ -1494,7 +1711,8 @@ window.__ModuleLoader__.load({
1494
1711
  const bucket = { hit: num(t.hit), miss: num(t.miss), out: num(t.out) }
1495
1712
  const at = t.time > 0 ? t.time : nowMs
1496
1713
  const peak = isPeakAt(at)
1497
- const price = priceFor(t.model || modelFallback)
1714
+ // 每轮按**它自己用的模型**计价(多模型混跑时不会串价)。
1715
+ const price = priceFor(t.model, modelFallback)
1498
1716
  series.push({
1499
1717
  turn: t.turn,
1500
1718
  hit: bucket.hit,
@@ -1503,7 +1721,12 @@ window.__ModuleLoader__.load({
1503
1721
  tokens: bucket.hit + bucket.miss + bucket.out,
1504
1722
  cost: costOf(bucket, price.table, peak),
1505
1723
  peak: peak,
1724
+ peakPricing: price.peakPricing,
1506
1725
  model: price.id,
1726
+ rawModel: price.raw,
1727
+ version: price.version,
1728
+ label: price.label,
1729
+ color: price.color,
1507
1730
  known: price.known,
1508
1731
  cached: !!t.cached,
1509
1732
  time: at,
@@ -1536,10 +1759,8 @@ window.__ModuleLoader__.load({
1536
1759
  function yCost(v) { return CHART_PAD.t + innerH - (v / topCost) * innerH }
1537
1760
 
1538
1761
  const tokenPts = []
1539
- const costPts = []
1540
1762
  for (let i = 0; i < n; i += 1) {
1541
1763
  tokenPts.push(xAt(i).toFixed(2) + ',' + yToken(series[i].tokens).toFixed(2))
1542
- costPts.push(xAt(i).toFixed(2) + ',' + yCost(series[i].cost).toFixed(2))
1543
1764
  }
1544
1765
  const grid = []
1545
1766
  const leftLabels = []
@@ -1557,11 +1778,49 @@ window.__ModuleLoader__.load({
1557
1778
  if (i % step !== 0 && i !== n - 1) continue
1558
1779
  xLabels.push(h('text', { key: 'x' + i, x: xAt(i), y: CHART_H - 6, textAnchor: 'middle', className: 'dcb-axis-label' }, '#' + String(series[i].turn)))
1559
1780
  }
1560
- const dots = []
1781
+ // 每个模型一条金额折线(各自颜色);该模型没有数据的轮次断开,不做插值连线。
1782
+ const byModel = {}
1783
+ const modelOrder = []
1561
1784
  for (let i = 0; i < n; i += 1) {
1562
- const active = hover === i
1563
- dots.push(h('circle', { key: 'dt' + i, cx: xAt(i), cy: yToken(series[i].tokens), r: active ? 4 : 2.2, className: 'dcb-dot' }))
1564
- dots.push(h('circle', { key: 'dc' + i, cx: xAt(i), cy: yCost(series[i].cost), r: active ? 4 : 2.2, className: 'dcb-dot dcb-dot-cost' }))
1785
+ const key = series[i].model || DEFAULT_MODEL
1786
+ if (byModel[key] === undefined) {
1787
+ byModel[key] = []
1788
+ modelOrder.push(key)
1789
+ }
1790
+ byModel[key].push(i)
1791
+ }
1792
+ const modelLines = []
1793
+ const dots = []
1794
+ for (let m = 0; m < modelOrder.length; m += 1) {
1795
+ const key = modelOrder[m]
1796
+ const idxs = byModel[key]
1797
+ const color = series[idxs[0]].color || '#4d6bfe'
1798
+ let d = ''
1799
+ let run = false
1800
+ for (let k = 0; k < n; k += 1) {
1801
+ const idx = byModel[key].indexOf(k)
1802
+ if (idx < 0) {
1803
+ run = false
1804
+ continue
1805
+ }
1806
+ const x = xAt(k)
1807
+ const y = yCost(series[k].cost)
1808
+ d += (run ? 'L' : 'M') + x.toFixed(2) + ' ' + y.toFixed(2) + ' '
1809
+ run = true
1810
+ }
1811
+ modelLines.push(h('path', {
1812
+ key: 'ml' + key,
1813
+ d: d.trim(),
1814
+ className: 'dcb-line dcb-line-cost',
1815
+ stroke: color,
1816
+ fill: 'none',
1817
+ }))
1818
+ for (let k = 0; k < idxs.length; k += 1) {
1819
+ const i2 = idxs[k]
1820
+ const active = hover === i2
1821
+ dots.push(h('circle', { key: 'dt' + i2, cx: xAt(i2), cy: yToken(series[i2].tokens), r: active ? 4 : 2.2, className: 'dcb-dot' }))
1822
+ dots.push(h('circle', { key: 'dc' + i2, cx: xAt(i2), cy: yCost(series[i2].cost), r: active ? 4 : 2.2, className: 'dcb-dot dcb-dot-cost', fill: color }))
1823
+ }
1565
1824
  }
1566
1825
  const hovered = hover >= 0 && hover < n ? series[hover] : null
1567
1826
  const crosshair = hovered
@@ -1583,8 +1842,12 @@ window.__ModuleLoader__.load({
1583
1842
  ? h('div', { className: 'dcb-tooltip', style: { left: String((xAt(hover) / CHART_W) * 100) + '%' } },
1584
1843
  h('div', { className: 'dcb-tooltip-title' },
1585
1844
  '#' + String(hovered.turn),
1586
- h('span', { className: 'dcb-tooltip-model' }, hovered.model || DEFAULT_MODEL),
1587
- h('span', { className: hovered.peak ? 'dcb-tag is-peak' : 'dcb-tag is-valley' }, tt(hovered.peak ? '峰时' : '谷时'))),
1845
+ h('span', { className: 'dcb-tooltip-model', title: modelDetail(hovered.rawModel || hovered.model) },
1846
+ h('i', { className: 'dcb-model-dot', style: { background: hovered.color } }),
1847
+ (hovered.label || hovered.model || DEFAULT_MODEL)
1848
+ + (hovered.version ? ' · ' + hovered.version : '')
1849
+ + (hovered.rawModel && hovered.rawModel !== hovered.model ? ' · ' + hovered.rawModel : '')),
1850
+ h('span', { className: hovered.peak && hovered.peakPricing !== false ? 'dcb-tag is-peak' : 'dcb-tag is-valley' }, tt(hovered.peak ? '峰时' : '谷时'))),
1588
1851
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输入命中')), h('b', null, fmtTokens(hovered.hit))),
1589
1852
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输入未命中')), h('b', null, fmtTokens(hovered.miss))),
1590
1853
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输出')), h('b', null, fmtTokens(hovered.out))),
@@ -1594,8 +1857,15 @@ window.__ModuleLoader__.load({
1594
1857
 
1595
1858
  return h('div', { className: 'dcb-chart-wrap' },
1596
1859
  h('div', { className: 'dcb-chart-legend' },
1597
- h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot' }), tt('每轮 Token')),
1598
- h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot dcb-dot-cost' }), tt('每轮估算金额'))),
1860
+ h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot dcb-dot-token' }), tt('每轮 Token')),
1861
+ modelOrder.length > 1
1862
+ ? modelOrder.map(function (key) {
1863
+ const first = series[byModel[key][0]]
1864
+ return h('span', { key: 'lg' + key, className: 'dcb-legend-item' },
1865
+ h('i', { className: 'dcb-legend-dot', style: { background: first.color } }),
1866
+ first.label || key)
1867
+ })
1868
+ : h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot dcb-dot-cost' }), tt('每轮估算金额'))),
1599
1869
  h('div', { className: 'dcb-chart-body' },
1600
1870
  h('svg', {
1601
1871
  className: 'dcb-chart-svg',
@@ -1606,8 +1876,8 @@ window.__ModuleLoader__.load({
1606
1876
  },
1607
1877
  h('g', null, grid),
1608
1878
  crosshair,
1609
- h('polyline', { className: 'dcb-line', points: tokenPts.join(' ') }),
1610
- h('polyline', { className: 'dcb-line dcb-line-cost', points: costPts.join(' ') }),
1879
+ h('polyline', { className: 'dcb-line dcb-line-token', points: tokenPts.join(' ') }),
1880
+ h('g', null, modelLines),
1611
1881
  h('g', null, dots),
1612
1882
  h('g', null, leftLabels),
1613
1883
  h('g', null, rightLabels),
@@ -1737,12 +2007,14 @@ window.__ModuleLoader__.load({
1737
2007
  }
1738
2008
  // 还没有任何数字时显示占位符,而不是误导性的 0。
1739
2009
  const pending = bucket === null && state.turnsState !== 'ready'
2010
+ // 计价兜底模型:会话日志里查不到模型时用它(设置页下拉选的那个 → 账号当前模型 → 默认)。
1740
2011
  const model = bucketModel
2012
+ || state.modelFilter
1741
2013
  || (state.account && typeof state.account.model === 'string' && state.account.model ? state.account.model : DEFAULT_MODEL)
1742
2014
  const at = bucketTime > 0 ? bucketTime : Date.now()
1743
2015
  const peak = isPeakAt(at)
1744
2016
  const tokens = bucket === null ? 0 : bucket.hit + bucket.miss + bucket.out
1745
- const cost = bucket === null ? 0 : costOf(bucket, priceFor(model).table, peak)
2017
+ const cost = bucket === null ? 0 : costOf(bucket, priceFor(bucketModel, state.modelFilter).table, peak)
1746
2018
 
1747
2019
  // 账户余额:成功 → ¥金额;读取中 → …;失败/未配置 → —(原因放 title,不挤占胶囊宽度)。
1748
2020
  const balance = state.balance
@@ -1807,13 +2079,26 @@ window.__ModuleLoader__.load({
1807
2079
 
1808
2080
  const now = Date.now()
1809
2081
  const surface = modalSurface(state.modalStyle, state.modalImageUrl)
1810
- const model = state.account && typeof state.account.model === 'string' && state.account.model ? state.account.model : DEFAULT_MODEL
2082
+ const accountModel = state.account && typeof state.account.model === 'string' && state.account.model ? state.account.model : DEFAULT_MODEL
2083
+ // 面板里的「模型」下拉 = 计价兜底模型(日志查不到模型时用它)+ 图例口径。
2084
+ const model = state.modelFilter || accountModel
1811
2085
  const price = priceFor(model)
1812
2086
  const peak = isPeakAt(now)
1813
- const series = buildSeries(state.turns, model, now)
1814
- const aggregated = bucketOfTurns(state.turns)
2087
+ const allSeries = buildSeries(state.turns, model, now)
2088
+ // 只看选中模型时,卡片/图表都按该模型过滤(选「全部模型」时不过滤)。
2089
+ const series = state.modelFilter ? allSeries.filter(function (s) { return s.model === state.modelFilter }) : allSeries
2090
+ const aggregated = { hit: 0, miss: 0, out: 0 }
2091
+ let totalCost = 0
2092
+ for (let i = 0; i < series.length; i += 1) {
2093
+ aggregated.hit += series[i].hit
2094
+ aggregated.miss += series[i].miss
2095
+ aggregated.out += series[i].out
2096
+ totalCost += series[i].cost
2097
+ }
1815
2098
  const tokens = aggregated.hit + aggregated.miss + aggregated.out
1816
- const totalCost = costOf(aggregated, price.table, peak)
2099
+ const modelSet = {}
2100
+ for (let i = 0; i < allSeries.length; i += 1) modelSet[allSeries[i].model] = allSeries[i]
2101
+ const modelsUsed = Object.keys(modelSet).map(function (k) { return modelSet[k] })
1817
2102
  const balance = state.balance
1818
2103
  const loading = state.balanceState === 'loading'
1819
2104
  const balanceErr = balanceErrorText()
@@ -1837,6 +2122,13 @@ window.__ModuleLoader__.load({
1837
2122
  if (typeof balance.granted === 'number') parts.push(tt('赠送 {amount}', { amount: moneyBalance(balance.granted, balance.currency) }))
1838
2123
  if (typeof balance.toppedUp === 'number') parts.push(tt('充值 {amount}', { amount: moneyBalance(balance.toppedUp, balance.currency) }))
1839
2124
  balanceHint = parts.length ? parts.join(' · ') : tt('DeepSeek 账户余额')
2125
+ // 选了模型时,把余额换算成「按该模型还能跑多少 token」——这就是余额显示与模型的联动。
2126
+ const perMillion = price.table.hit * 0.5 + price.table.miss * 0.3 + price.table.out * 0.2
2127
+ if (typeof balance.total === 'number' && perMillion > 0 && balance.currency === 'CNY') {
2128
+ const affordable = (balance.total / perMillion) * 1e6
2129
+ balanceHint = (parts.length ? parts.join(' · ') + ' · ' : '')
2130
+ + tt('按 {model} 约还可跑 {tokens} tokens', { model: price.label, tokens: fmtTokens(affordable) })
2131
+ }
1840
2132
  }
1841
2133
 
1842
2134
  function close() {
@@ -1870,11 +2162,31 @@ window.__ModuleLoader__.load({
1870
2162
  h('div', { className: 'dcb-head' },
1871
2163
  h('div', { className: 'dcb-head-title' }, tt('⛽ 本会话消耗')),
1872
2164
  h('div', { className: 'dcb-head-actions' },
2165
+ // 模型下拉:只看某个模型,同时作为「查不到模型时」的计价基准。
2166
+ h('select', {
2167
+ className: 'dcb-select dcb-select-inline',
2168
+ 'aria-label': tt('模型'),
2169
+ title: tt('按模型筛选;同时作为计价兜底'),
2170
+ value: state.modelFilter,
2171
+ onChange: function (event) { store.setModelFilter(event.target.value) },
2172
+ },
2173
+ h('option', { key: 'all', value: '' }, tt('全部模型')),
2174
+ modelOptionNodes(modelsUsed.length > 0
2175
+ ? modelsUsed.map(function (m) { return MODEL_BY_ID[m.model] || { id: m.model, label: m.label, provider: '—' } })
2176
+ : MODELS)),
1873
2177
  h('button', { type: 'button', className: 'dcb-btn', onClick: function () { store.ensureBalance(true) } }, tt('刷新余额')),
1874
2178
  h('button', { type: 'button', className: 'dcb-btn dcb-btn-close', onClick: close, title: tt('Esc 关闭') }, '✕'))),
1875
2179
  h('div', { className: 'dcb-cards' },
1876
2180
  h(Card, { label: tt('累计 Token'), value: fmtTokens(tokens), hint: tt('命中 {hit} · 未命中 {miss} · 输出 {out}', { hit: fmtTokens(aggregated.hit), miss: fmtTokens(aggregated.miss), out: fmtTokens(aggregated.out) }) }),
1877
- h(Card, { label: tt('估算金额'), value: moneyApprox(totalCost), hint: (price.known ? price.id : tt('{id}(按 flash 估算)', { id: price.id })) + tt(peak ? ' · 峰时 ×2' : ' · 谷时') }),
2181
+ h(Card, {
2182
+ label: tt('估算金额'),
2183
+ value: moneyApprox(totalCost),
2184
+ hint: (state.modelFilter
2185
+ ? tt('仅 {model}', { model: modelDetail(state.modelFilter) })
2186
+ : tt('{count} 个模型各自计价', { count: String(modelsUsed.length || 1) })
2187
+ + (modelsUsed.length === 1 ? ' · ' + modelDetail(modelsUsed[0].model) : ''))
2188
+ + (price.peakPricing ? tt(peak ? ' · 峰时 ×2' : ' · 谷时') : ''),
2189
+ }),
1878
2190
  h(Card, { label: tt('当前时段'), value: tt(peak ? '高峰' : '空闲'), hint: tt('北京时间 工作日 09:00-12:00 / 14:00-18:00') }),
1879
2191
  h('div', { className: 'dcb-card' },
1880
2192
  h('div', { className: 'dcb-card-label' }, tt('账户余额')),
@@ -1884,7 +2196,7 @@ window.__ModuleLoader__.load({
1884
2196
  h(UsageChart, { series: series }),
1885
2197
  h('div', { className: 'dcb-foot' },
1886
2198
  state.turnsError ? h('span', { className: 'dcb-foot-warn' }, turnsErrorText() + ' ') : null,
1887
- tt('金额按 DeepSeek 官方价估算(谷/峰 ×1/×2),仅供参考;数据源为本会话日志,刷新后自动恢复。'))))
2199
+ tt('金额按各模型官方价估算(DeepSeek 区分峰谷 ×1/×2,其它厂商按 USD 价 ×7.1 折人民币),仅供参考;数据源为本会话日志,刷新后自动恢复。'))))
1888
2200
  }
1889
2201
 
1890
2202
  /* ===================== settings section: history page ===================== */
@@ -1999,9 +2311,11 @@ window.__ModuleLoader__.load({
1999
2311
  }
2000
2312
 
2001
2313
  /**
2002
- * 纯聚合:时间过滤 → 工作区过滤 → 按小时/日/月分桶(空桶补 0)→ 峰谷与金额拆分。
2314
+ * 纯聚合:时间过滤 → 工作区/模型过滤 → 按小时/日/月分桶(空桶补 0)→ 峰谷与金额拆分 →
2315
+ * 每个桶再按模型拆一份 `byModel`(折线图一个模型一条线就靠它)。
2003
2316
  * 刻意自包含(只读 input,不引用闭包变量),所以能直接 String() 进 Web Worker。
2004
- * @param input {records, from, to, wid, granularity: 'hour'|'day'|'month', prices, holidays, windows}
2317
+ * @param input {records, from, to, wid, model, granularity: 'hour'|'day'|'month', prices, holidays, windows}
2318
+ * `model` 非空时只统计该模型;`prices[id]` 里的 `peak: true` 表示该模型参与峰谷计时。
2005
2319
  */
2006
2320
  function computeConsumptionStats(input) {
2007
2321
  const records = input.records || []
@@ -2011,6 +2325,7 @@ window.__ModuleLoader__.load({
2011
2325
  const from = typeof input.from === 'number' ? input.from : 0
2012
2326
  const to = typeof input.to === 'number' ? input.to : 0
2013
2327
  const wid = typeof input.wid === 'string' ? input.wid : ''
2328
+ const modelFilter = typeof input.model === 'string' ? input.model : ''
2014
2329
  const granularity = input.granularity === 'month' ? 'month' : (input.granularity === 'hour' ? 'hour' : 'day')
2015
2330
  // noFill:只要合计不要时间轴(设置页顶部的“累计”卡片用),跳过分桶补齐。
2016
2331
  const noFill = input.noFill === true
@@ -2042,7 +2357,10 @@ window.__ModuleLoader__.load({
2042
2357
  function priceOf(model) {
2043
2358
  const id = typeof model === 'string' ? model : ''
2044
2359
  if (prices[id]) return prices[id]
2045
- if (id.toLowerCase().indexOf('pro') >= 0) return prices['deepseek-v4-pro'] || prices['deepseek-flash']
2360
+ // 兼容历史 id:pro/opus/max 归到更贵的一档,其余按 flash。
2361
+ if (id.toLowerCase().indexOf('pro') >= 0 || id.toLowerCase().indexOf('opus') >= 0 || id.toLowerCase().indexOf('max') >= 0) {
2362
+ return prices['deepseek-v4-pro'] || prices['deepseek-flash']
2363
+ }
2046
2364
  return prices['deepseek-flash']
2047
2365
  }
2048
2366
  function keyOf(ms) {
@@ -2070,14 +2388,17 @@ window.__ModuleLoader__.load({
2070
2388
  const produced = n(r.out)
2071
2389
  const tokens = hit + miss + produced
2072
2390
  if (tokens === 0) continue
2391
+ const rowModel = typeof r.model === 'string' && r.model ? r.model : ''
2392
+ if (modelFilter && rowModel !== modelFilter) continue
2073
2393
  const peak = isPeak(t)
2074
- const table = priceOf(r.model)
2075
- const k = peak ? 2 : 1
2394
+ const table = priceOf(rowModel)
2395
+ // 只有 `peak: true` 的模型(DeepSeek)才乘峰时系数。
2396
+ const k = peak && table.peak === true ? 2 : 1
2076
2397
  const cost = ((hit / 1e6) * table.hit + (miss / 1e6) * table.miss + (produced / 1e6) * table.out) * k
2077
2398
  const key = keyOf(t)
2078
2399
  let b = buckets[key]
2079
2400
  if (!b) {
2080
- b = { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0 }
2401
+ b = { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0, byModel: {} }
2081
2402
  buckets[key] = b
2082
2403
  }
2083
2404
  b.tokens += tokens
@@ -2101,13 +2422,22 @@ window.__ModuleLoader__.load({
2101
2422
  }
2102
2423
  const model = typeof r.model === 'string' && r.model ? r.model : 'unknown'
2103
2424
  if (b.models.indexOf(model) < 0) b.models.push(model)
2425
+ // 桶内按模型拆分:cost/tokens 给折线图每个模型一条线用。
2426
+ let slot = b.byModel[model]
2427
+ if (!slot) {
2428
+ slot = { cost: 0, tokens: 0, records: 0 }
2429
+ b.byModel[model] = slot
2430
+ }
2431
+ slot.cost += cost
2432
+ slot.tokens += tokens
2433
+ slot.records += 1
2104
2434
  count += 1
2105
2435
  }
2106
2436
 
2107
2437
  const points = []
2108
2438
  function push(key, ms) {
2109
2439
  const b = buckets[key]
2110
- const point = b || { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0 }
2440
+ const point = b || { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0, byModel: {} }
2111
2441
  if (granularity === 'month') point.label = key
2112
2442
  else if (granularity === 'hour') point.label = key.slice(11) + ':00'
2113
2443
  else point.label = key.slice(5)
@@ -2244,10 +2574,8 @@ window.__ModuleLoader__.load({
2244
2574
  function yToken(v) { return HIST_PAD.t + innerH - (v / topToken) * innerH }
2245
2575
  function yCost(v) { return HIST_PAD.t + innerH - (v / topCost) * innerH }
2246
2576
 
2247
- const costPts = []
2248
2577
  const tokenPts = []
2249
2578
  for (let i = 0; i < n; i += 1) {
2250
- costPts.push(xAt(i).toFixed(2) + ',' + yCost(points[i].cost).toFixed(2))
2251
2579
  tokenPts.push(xAt(i).toFixed(2) + ',' + yToken(points[i].tokens).toFixed(2))
2252
2580
  }
2253
2581
 
@@ -2272,13 +2600,47 @@ window.__ModuleLoader__.load({
2272
2600
  }
2273
2601
 
2274
2602
  const dots = []
2603
+ // 每个模型一条金额折线(各自颜色):该桶里没有这个模型就断线。
2604
+ const modelUsage = {}
2605
+ const modelOrder = []
2275
2606
  for (let i = 0; i < n; i += 1) {
2276
- const active = hover === i
2277
- if (points[i].records > 0) {
2278
- dots.push(h('circle', { key: 'hc' + i, cx: xAt(i), cy: yCost(points[i].cost), r: active ? 4 : 2.2, className: 'dcb-dot dcb-dot-cost' }))
2607
+ const byModel = points[i].byModel || {}
2608
+ for (const key in byModel) {
2609
+ if (!Object.prototype.hasOwnProperty.call(byModel, key)) continue
2610
+ if (modelUsage[key] === undefined) {
2611
+ modelUsage[key] = []
2612
+ modelOrder.push(key)
2613
+ }
2614
+ modelUsage[key].push(i)
2279
2615
  }
2616
+ }
2617
+ const modelLines = []
2618
+ for (let m = 0; m < modelOrder.length; m += 1) {
2619
+ const key = modelOrder[m]
2620
+ const rows = modelUsage[key]
2621
+ const color = modelColor(key)
2622
+ let d = ''
2623
+ for (let k = 0; k < rows.length; k += 1) {
2624
+ const idx = rows[k]
2625
+ const seg = points[idx].byModel[key]
2626
+ d += (k === 0 || rows[k - 1] !== idx - 1 ? 'M' : 'L') + xAt(idx).toFixed(2) + ' ' + yCost(seg.cost).toFixed(2) + ' '
2627
+ }
2628
+ modelLines.push(h('path', { key: 'hml' + key, d: d.trim(), className: 'dcb-line dcb-line-cost', stroke: color, fill: 'none' }))
2629
+ for (let k = 0; k < rows.length; k += 1) {
2630
+ const idx = rows[k]
2631
+ dots.push(h('circle', {
2632
+ key: 'hc' + key + idx,
2633
+ cx: xAt(idx),
2634
+ cy: yCost(points[idx].byModel[key].cost),
2635
+ r: hover === idx ? 4 : 2.2,
2636
+ className: 'dcb-dot dcb-dot-cost',
2637
+ fill: color,
2638
+ }))
2639
+ }
2640
+ }
2641
+ for (let i = 0; i < n; i += 1) {
2280
2642
  if (showTokens && points[i].records > 0) {
2281
- dots.push(h('circle', { key: 'ht' + i, cx: xAt(i), cy: yToken(points[i].tokens), r: active ? 4 : 2.2, className: 'dcb-dot' }))
2643
+ dots.push(h('circle', { key: 'ht' + i, cx: xAt(i), cy: yToken(points[i].tokens), r: hover === i ? 4 : 2.2, className: 'dcb-dot' }))
2282
2644
  }
2283
2645
  }
2284
2646
 
@@ -2307,7 +2669,9 @@ window.__ModuleLoader__.load({
2307
2669
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, 'Token'), h('b', null, fmtTokens(hovered.tokens))),
2308
2670
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('峰时')), h('b', null, moneyApprox(hovered.peakCost) + ' / ' + fmtTokens(hovered.peakTokens))),
2309
2671
  h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('谷时')), h('b', null, moneyApprox(hovered.valleyCost) + ' / ' + fmtTokens(hovered.valleyTokens))),
2310
- h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('模型')), h('b', null, hovered.models.length ? hovered.models.join(', ') : '—')))
2672
+ h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('模型')), h('b', null, hovered.models.length
2673
+ ? hovered.models.map(function (m) { return modelLabel(m) }).join(', ')
2674
+ : '—')))
2311
2675
  : null
2312
2676
 
2313
2677
  return h('div', { className: 'dcb-chart-body' },
@@ -2320,8 +2684,8 @@ window.__ModuleLoader__.load({
2320
2684
  },
2321
2685
  h('g', null, grid),
2322
2686
  crosshair,
2323
- showTokens ? h('polyline', { className: 'dcb-line', points: tokenPts.join(' ') }) : null,
2324
- h('polyline', { className: 'dcb-line dcb-line-cost', points: costPts.join(' ') }),
2687
+ showTokens ? h('polyline', { className: 'dcb-line dcb-line-token', points: tokenPts.join(' ') }) : null,
2688
+ h('g', null, modelLines),
2325
2689
  h('g', null, dots),
2326
2690
  h('g', null, leftLabels),
2327
2691
  h('g', null, rightLabels),
@@ -2358,6 +2722,9 @@ window.__ModuleLoader__.load({
2358
2722
  const s8 = React.useState(null)
2359
2723
  const allStats = s8[0]
2360
2724
  const setAllStats = s8[1]
2725
+ // 「模型」下拉的状态放在 store 里(弹层与设置页共享同一个选择)。
2726
+ const modelFilter = store.state.modelFilter
2727
+ const language = store.state.language
2361
2728
  const s9 = React.useState(0)
2362
2729
  const setStoreTick = s9[1]
2363
2730
 
@@ -2408,6 +2775,7 @@ window.__ModuleLoader__.load({
2408
2775
  from: range.from,
2409
2776
  to: range.to,
2410
2777
  wid: wid,
2778
+ model: modelFilter,
2411
2779
  granularity: range.granularity,
2412
2780
  prices: PRICES,
2413
2781
  holidays: CN_FIXED_HOLIDAYS,
@@ -2423,7 +2791,7 @@ window.__ModuleLoader__.load({
2423
2791
  setStats(fallback)
2424
2792
  })
2425
2793
  return undefined
2426
- }, [records, rangeKey, wid, calFrom, calTo, wsSig])
2794
+ }, [records, rangeKey, wid, calFrom, calTo, wsSig, modelFilter])
2427
2795
 
2428
2796
  // 顶部“累计”卡片:全部历史合计(不受筛选影响),只算合计不铺时间轴。
2429
2797
  React.useEffect(function () {
@@ -2480,7 +2848,8 @@ window.__ModuleLoader__.load({
2480
2848
  rateNote = tt('金额按人民币报价折算显示')
2481
2849
  }
2482
2850
  const accountModel = st.account && typeof st.account.model === 'string' && st.account.model ? st.account.model : DEFAULT_MODEL
2483
- const accountPrice = priceFor(accountModel)
2851
+ // 「模型」下拉选了具体模型时,它同时是计价兜底(日志里查不到模型时用它)。
2852
+ const accountPrice = priceFor(modelFilter || accountModel)
2484
2853
  const nowPeak = isPeakAt(Date.now())
2485
2854
  const balance = st.balance
2486
2855
  const balanceLoading = st.balanceState === 'loading'
@@ -2510,6 +2879,15 @@ window.__ModuleLoader__.load({
2510
2879
  const index = buildWorkspaceIndex(wsSig)
2511
2880
  const points = stats ? stats.points : []
2512
2881
  const hasData = !!stats && stats.count > 0
2882
+ // 图例:这一区间里出现过的模型(按聚合结果的 byModel 收集,颜色与折线一致)。
2883
+ const legendModels = []
2884
+ for (let pi = 0; pi < points.length; pi += 1) {
2885
+ const byModel = points[pi].byModel || {}
2886
+ for (const key in byModel) {
2887
+ if (!Object.prototype.hasOwnProperty.call(byModel, key)) continue
2888
+ if (legendModels.indexOf(key) < 0) legendModels.push(key)
2889
+ }
2890
+ }
2513
2891
  const rangeOptions = [
2514
2892
  { id: 'today', label: tt('今天') },
2515
2893
  { id: 'week', label: tt('近一周') },
@@ -2533,7 +2911,7 @@ window.__ModuleLoader__.load({
2533
2911
  h(Card, {
2534
2912
  label: tt('估算金额'),
2535
2913
  value: allStats ? moneyApprox(allStats.totalCost) : '—',
2536
- hint: (accountPrice.known ? accountPrice.id : tt('{id}(按 flash 估算)', { id: accountPrice.id })) + tt(' · 全部历史逐轮计价'),
2914
+ hint: (accountPrice.known ? modelDetail(modelFilter || accountModel) : tt('{id}(按 flash 估算)', { id: accountPrice.id })) + tt(' · 全部历史逐轮计价'),
2537
2915
  }),
2538
2916
  h(Card, {
2539
2917
  label: tt('当前时段'),
@@ -2592,6 +2970,29 @@ window.__ModuleLoader__.load({
2592
2970
  h('div', { className: 'dcb-field' },
2593
2971
  h('span', { className: 'dcb-field-label' }, tt('数据')),
2594
2972
  h('button', { type: 'button', className: 'dcb-btn', onClick: function () { setReloadTick(function (v) { return v + 1 }) } }, tt('刷新'))),
2973
+ // 模型筛选:只看某个模型的消耗;同时作为「日志查不到模型」时的计价基准。
2974
+ h('div', { className: 'dcb-field' },
2975
+ h('label', { className: 'dcb-field-label', htmlFor: 'dcb-model-select' }, tt('模型')),
2976
+ h('select', {
2977
+ id: 'dcb-model-select',
2978
+ className: 'dcb-select',
2979
+ value: modelFilter,
2980
+ onChange: function (event) { store.setModelFilter(event.target.value) },
2981
+ },
2982
+ h('option', { key: 'all', value: '' }, tt('全部模型')),
2983
+ modelOptionNodes(MODELS))),
2984
+ // 语言:默认跟随外壳;外壳语言不对劲时可以直接钉死中文/英文。
2985
+ h('div', { className: 'dcb-field' },
2986
+ h('label', { className: 'dcb-field-label', htmlFor: 'dcb-language-select' }, tt('语言')),
2987
+ h('select', {
2988
+ id: 'dcb-language-select',
2989
+ className: 'dcb-select',
2990
+ value: language,
2991
+ onChange: function (event) { store.setLanguage(event.target.value) },
2992
+ },
2993
+ languageOptions().map(function (opt) {
2994
+ return h('option', { key: opt.id || 'auto', value: opt.id }, opt.label)
2995
+ }))),
2595
2996
  // 显示币种:报价表永远是人民币,这里只决定展示时换算成什么货币。
2596
2997
  h('div', { className: 'dcb-field' },
2597
2998
  h('label', { className: 'dcb-field-label', htmlFor: 'dcb-currency-select' }, tt('货币')),
@@ -2607,7 +3008,14 @@ window.__ModuleLoader__.load({
2607
3008
  h('div', { className: 'dcb-set-note dcb-rate-note' }, rateNote),
2608
3009
  h('div', { className: 'dcb-set-body' },
2609
3010
  h('div', { className: 'dcb-hist-legend' },
2610
- h('span', { className: 'dcb-tog dcb-tog-cost is-on' }, h('i', null), tt('估算金额(右轴)')),
3011
+ // 每个模型一枚颜色 chip(与折线同色),右侧是 Token 轴开关。
3012
+ legendModels.length > 0
3013
+ ? legendModels.map(function (key) {
3014
+ return h('span', { key: 'lg' + key, className: 'dcb-legend-chip', title: modelDetail(key) },
3015
+ h('i', { className: 'dcb-legend-dot', style: { background: modelColor(key) } }),
3016
+ modelLabel(key))
3017
+ })
3018
+ : h('span', { className: 'dcb-tog dcb-tog-cost is-on' }, h('i', null), tt('估算金额(右轴)')),
2611
3019
  h('button', {
2612
3020
  type: 'button',
2613
3021
  className: showTokens ? 'dcb-tog is-on' : 'dcb-tog',
@@ -3068,6 +3476,14 @@ window.__ModuleLoader__.load({
3068
3476
  .dcb-legend-item{display:inline-flex;align-items:center;gap:6px}
3069
3477
  .dcb-legend-dot{display:inline-block;width:14px;height:3px;border-radius:2px;background:var(--dsw-alias-brand-primary)}
3070
3478
  .dcb-legend-dot.dcb-dot-cost{background:var(--dsw-alias-state-success-primary)}
3479
+ /* 图例里的模型 chip(与折线同色) */
3480
+ .dcb-legend-chip{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
3481
+ .dcb-legend-chip .dcb-legend-dot{width:9px;height:9px;border-radius:50%}
3482
+ /* 弹层标题栏里的模型下拉 */
3483
+ .dcb-select-inline{height:22px;font-size:11px;min-width:118px;max-width:190px;padding:0 6px;border-radius:6px}
3484
+ .dcb-select-inline:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}
3485
+ /* tooltip 里的模型色点 */
3486
+ .dcb-model-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:middle}
3071
3487
  .dcb-chart-body{position:relative}
3072
3488
  .dcb-chart-svg{display:block;width:100%;height:216px}
3073
3489
  .dcb-grid-line{stroke:var(--dsw-alias-border-l1);stroke-width:1;opacity:.7;vector-effect:non-scaling-stroke}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-consumption-badge",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "DSH Web UI plugin: a per-message consumption badge (tokens | estimated cost | peak/valley) plus a session detail modal with cumulative tokens, estimated cost, Beijing-time peak/off-peak pricing, a per-turn dual-axis chart, and the DeepSeek account balance",
5
5
  "keywords": [
6
6
  "deepseek-harness",