dsh-consumption-badge 1.3.1 → 1.3.2
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/README.md +53 -6
- package/lib/client.js +403 -52
- 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)
|
|
6
|
-
**
|
|
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
|
|
30
|
-
(left axis: tokens, right axis:
|
|
31
|
-
|
|
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
|
|
@@ -374,7 +421,7 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
|
|
|
374
421
|
- Do not hard-code user-visible English or Chinese in new JSX: call `tt('…')` with the Chinese copy
|
|
375
422
|
as the key and add the English entry to `DICT_EN`. `check.mjs` fails if any `tt()` key has no
|
|
376
423
|
English translation or if a translation still contains Chinese.
|
|
377
|
-
- Self-check without booting DSH: `node check.mjs` (
|
|
424
|
+
- Self-check without booting DSH: `node check.mjs` (320 assertions over the route contract, the
|
|
378
425
|
client bundle contract, the pricing/aggregation maths and the bilingual copy). It lives outside
|
|
379
426
|
the published `files` list.
|
|
380
427
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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,14 @@ 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
|
+
'{count} 个模型各自计价': '{count} models priced separately',
|
|
438
|
+
'按 {model} 约还可跑 {tokens} tokens': 'about {tokens} tokens left at {model} prices',
|
|
439
|
+
'金额按各模型官方价估算(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
440
|
'金额按人民币报价折算显示': 'Amounts are priced in CNY and converted for display',
|
|
290
441
|
'当前以人民币显示(可切换成其它币种)': 'Showing CNY — pick another currency to convert',
|
|
291
442
|
'1 CNY = {rate} {code}': '1 CNY = {rate} {code}',
|
|
@@ -648,10 +799,10 @@ window.__ModuleLoader__.load({
|
|
|
648
799
|
|
|
649
800
|
/* ================= 消耗弹窗背景样式(与个性化插件联动) ================= */
|
|
650
801
|
const MODAL_STYLE_KEY = 'dsh-consumption:modal-style'
|
|
651
|
-
const BUILD_TAG = 'dcb-1.3.
|
|
802
|
+
const BUILD_TAG = 'dcb-1.3.2'
|
|
652
803
|
// 本地版本:本包是零构建的手写客户端包,无法 import package.json,因此把版本号作为常量维护,
|
|
653
804
|
// 并由 check.mjs 守卫与 package.json 严格一致(发布前不一致直接失败)。
|
|
654
|
-
const PLUGIN_VERSION = '1.3.
|
|
805
|
+
const PLUGIN_VERSION = '1.3.2'
|
|
655
806
|
const PKG_NAME = 'dsh-consumption-badge'
|
|
656
807
|
const UPDATE_REGISTRY_URL = 'https://registry.npmjs.org/' + PKG_NAME + '/latest'
|
|
657
808
|
const UPDATE_COMMAND = 'dsh plugin --profile web update ' + PKG_NAME
|
|
@@ -1042,6 +1193,8 @@ window.__ModuleLoader__.load({
|
|
|
1042
1193
|
modalStyle: readModalStyle(),
|
|
1043
1194
|
// 显示币种(本地偏好)+ 汇率(Host 路由,6h 缓存):金额展示都走 money()。
|
|
1044
1195
|
currency: readCurrency(),
|
|
1196
|
+
// 模型筛选 / 计价兜底('' = 全部模型,不筛选;同时作为查不到模型时的计价基准)。
|
|
1197
|
+
modelFilter: readModelFilter(),
|
|
1045
1198
|
rates: null,
|
|
1046
1199
|
ratesState: 'idle',
|
|
1047
1200
|
ratesError: '',
|
|
@@ -1300,6 +1453,17 @@ window.__ModuleLoader__.load({
|
|
|
1300
1453
|
state.rates = null
|
|
1301
1454
|
return ensureRates(true)
|
|
1302
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 切换「模型」下拉:'' = 全部模型(不筛选);
|
|
1458
|
+
* 选了具体模型时,它同时是**统计筛选条件**和**计价兜底模型**。
|
|
1459
|
+
*/
|
|
1460
|
+
function setModelFilter(id) {
|
|
1461
|
+
const next = typeof id === 'string' && MODEL_BY_ID[id] ? id : ''
|
|
1462
|
+
if (state.modelFilter === next) return
|
|
1463
|
+
state.modelFilter = next
|
|
1464
|
+
lsSet(LS_MODEL_FILTER, next)
|
|
1465
|
+
emit()
|
|
1466
|
+
}
|
|
1303
1467
|
/** 更新弹窗背景配置:合并 → 落本地偏好 → 通知所有订阅者(弹层即时重绘)。 */
|
|
1304
1468
|
function setModalStyle(patch) {
|
|
1305
1469
|
const prev = state.modalStyle
|
|
@@ -1462,6 +1626,7 @@ window.__ModuleLoader__.load({
|
|
|
1462
1626
|
ensureRates: ensureRates,
|
|
1463
1627
|
setCurrency: setCurrency,
|
|
1464
1628
|
refreshRates: refreshRates,
|
|
1629
|
+
setModelFilter: setModelFilter,
|
|
1465
1630
|
openPanel: openPanel,
|
|
1466
1631
|
closePanel: closePanel,
|
|
1467
1632
|
setModalStyle: setModalStyle,
|
|
@@ -1494,7 +1659,8 @@ window.__ModuleLoader__.load({
|
|
|
1494
1659
|
const bucket = { hit: num(t.hit), miss: num(t.miss), out: num(t.out) }
|
|
1495
1660
|
const at = t.time > 0 ? t.time : nowMs
|
|
1496
1661
|
const peak = isPeakAt(at)
|
|
1497
|
-
|
|
1662
|
+
// 每轮按**它自己用的模型**计价(多模型混跑时不会串价)。
|
|
1663
|
+
const price = priceFor(t.model, modelFallback)
|
|
1498
1664
|
series.push({
|
|
1499
1665
|
turn: t.turn,
|
|
1500
1666
|
hit: bucket.hit,
|
|
@@ -1503,7 +1669,12 @@ window.__ModuleLoader__.load({
|
|
|
1503
1669
|
tokens: bucket.hit + bucket.miss + bucket.out,
|
|
1504
1670
|
cost: costOf(bucket, price.table, peak),
|
|
1505
1671
|
peak: peak,
|
|
1672
|
+
peakPricing: price.peakPricing,
|
|
1506
1673
|
model: price.id,
|
|
1674
|
+
rawModel: price.raw,
|
|
1675
|
+
version: price.version,
|
|
1676
|
+
label: price.label,
|
|
1677
|
+
color: price.color,
|
|
1507
1678
|
known: price.known,
|
|
1508
1679
|
cached: !!t.cached,
|
|
1509
1680
|
time: at,
|
|
@@ -1536,10 +1707,8 @@ window.__ModuleLoader__.load({
|
|
|
1536
1707
|
function yCost(v) { return CHART_PAD.t + innerH - (v / topCost) * innerH }
|
|
1537
1708
|
|
|
1538
1709
|
const tokenPts = []
|
|
1539
|
-
const costPts = []
|
|
1540
1710
|
for (let i = 0; i < n; i += 1) {
|
|
1541
1711
|
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
1712
|
}
|
|
1544
1713
|
const grid = []
|
|
1545
1714
|
const leftLabels = []
|
|
@@ -1557,11 +1726,49 @@ window.__ModuleLoader__.load({
|
|
|
1557
1726
|
if (i % step !== 0 && i !== n - 1) continue
|
|
1558
1727
|
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
1728
|
}
|
|
1560
|
-
|
|
1729
|
+
// 每个模型一条金额折线(各自颜色);该模型没有数据的轮次断开,不做插值连线。
|
|
1730
|
+
const byModel = {}
|
|
1731
|
+
const modelOrder = []
|
|
1561
1732
|
for (let i = 0; i < n; i += 1) {
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1733
|
+
const key = series[i].model || DEFAULT_MODEL
|
|
1734
|
+
if (byModel[key] === undefined) {
|
|
1735
|
+
byModel[key] = []
|
|
1736
|
+
modelOrder.push(key)
|
|
1737
|
+
}
|
|
1738
|
+
byModel[key].push(i)
|
|
1739
|
+
}
|
|
1740
|
+
const modelLines = []
|
|
1741
|
+
const dots = []
|
|
1742
|
+
for (let m = 0; m < modelOrder.length; m += 1) {
|
|
1743
|
+
const key = modelOrder[m]
|
|
1744
|
+
const idxs = byModel[key]
|
|
1745
|
+
const color = series[idxs[0]].color || '#4d6bfe'
|
|
1746
|
+
let d = ''
|
|
1747
|
+
let run = false
|
|
1748
|
+
for (let k = 0; k < n; k += 1) {
|
|
1749
|
+
const idx = byModel[key].indexOf(k)
|
|
1750
|
+
if (idx < 0) {
|
|
1751
|
+
run = false
|
|
1752
|
+
continue
|
|
1753
|
+
}
|
|
1754
|
+
const x = xAt(k)
|
|
1755
|
+
const y = yCost(series[k].cost)
|
|
1756
|
+
d += (run ? 'L' : 'M') + x.toFixed(2) + ' ' + y.toFixed(2) + ' '
|
|
1757
|
+
run = true
|
|
1758
|
+
}
|
|
1759
|
+
modelLines.push(h('path', {
|
|
1760
|
+
key: 'ml' + key,
|
|
1761
|
+
d: d.trim(),
|
|
1762
|
+
className: 'dcb-line dcb-line-cost',
|
|
1763
|
+
stroke: color,
|
|
1764
|
+
fill: 'none',
|
|
1765
|
+
}))
|
|
1766
|
+
for (let k = 0; k < idxs.length; k += 1) {
|
|
1767
|
+
const i2 = idxs[k]
|
|
1768
|
+
const active = hover === i2
|
|
1769
|
+
dots.push(h('circle', { key: 'dt' + i2, cx: xAt(i2), cy: yToken(series[i2].tokens), r: active ? 4 : 2.2, className: 'dcb-dot' }))
|
|
1770
|
+
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 }))
|
|
1771
|
+
}
|
|
1565
1772
|
}
|
|
1566
1773
|
const hovered = hover >= 0 && hover < n ? series[hover] : null
|
|
1567
1774
|
const crosshair = hovered
|
|
@@ -1583,8 +1790,12 @@ window.__ModuleLoader__.load({
|
|
|
1583
1790
|
? h('div', { className: 'dcb-tooltip', style: { left: String((xAt(hover) / CHART_W) * 100) + '%' } },
|
|
1584
1791
|
h('div', { className: 'dcb-tooltip-title' },
|
|
1585
1792
|
'#' + String(hovered.turn),
|
|
1586
|
-
h('span', { className: 'dcb-tooltip-model'
|
|
1587
|
-
|
|
1793
|
+
h('span', { className: 'dcb-tooltip-model', title: modelDetail(hovered.rawModel || hovered.model) },
|
|
1794
|
+
h('i', { className: 'dcb-model-dot', style: { background: hovered.color } }),
|
|
1795
|
+
(hovered.label || hovered.model || DEFAULT_MODEL)
|
|
1796
|
+
+ (hovered.version ? ' · ' + hovered.version : '')
|
|
1797
|
+
+ (hovered.rawModel && hovered.rawModel !== hovered.model ? ' · ' + hovered.rawModel : '')),
|
|
1798
|
+
h('span', { className: hovered.peak && hovered.peakPricing !== false ? 'dcb-tag is-peak' : 'dcb-tag is-valley' }, tt(hovered.peak ? '峰时' : '谷时'))),
|
|
1588
1799
|
h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输入命中')), h('b', null, fmtTokens(hovered.hit))),
|
|
1589
1800
|
h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输入未命中')), h('b', null, fmtTokens(hovered.miss))),
|
|
1590
1801
|
h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('输出')), h('b', null, fmtTokens(hovered.out))),
|
|
@@ -1594,8 +1805,15 @@ window.__ModuleLoader__.load({
|
|
|
1594
1805
|
|
|
1595
1806
|
return h('div', { className: 'dcb-chart-wrap' },
|
|
1596
1807
|
h('div', { className: 'dcb-chart-legend' },
|
|
1597
|
-
h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot' }), tt('每轮 Token')),
|
|
1598
|
-
|
|
1808
|
+
h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot dcb-dot-token' }), tt('每轮 Token')),
|
|
1809
|
+
modelOrder.length > 1
|
|
1810
|
+
? modelOrder.map(function (key) {
|
|
1811
|
+
const first = series[byModel[key][0]]
|
|
1812
|
+
return h('span', { key: 'lg' + key, className: 'dcb-legend-item' },
|
|
1813
|
+
h('i', { className: 'dcb-legend-dot', style: { background: first.color } }),
|
|
1814
|
+
first.label || key)
|
|
1815
|
+
})
|
|
1816
|
+
: h('span', { className: 'dcb-legend-item' }, h('i', { className: 'dcb-legend-dot dcb-dot-cost' }), tt('每轮估算金额'))),
|
|
1599
1817
|
h('div', { className: 'dcb-chart-body' },
|
|
1600
1818
|
h('svg', {
|
|
1601
1819
|
className: 'dcb-chart-svg',
|
|
@@ -1606,8 +1824,8 @@ window.__ModuleLoader__.load({
|
|
|
1606
1824
|
},
|
|
1607
1825
|
h('g', null, grid),
|
|
1608
1826
|
crosshair,
|
|
1609
|
-
h('polyline', { className: 'dcb-line', points: tokenPts.join(' ') }),
|
|
1610
|
-
h('
|
|
1827
|
+
h('polyline', { className: 'dcb-line dcb-line-token', points: tokenPts.join(' ') }),
|
|
1828
|
+
h('g', null, modelLines),
|
|
1611
1829
|
h('g', null, dots),
|
|
1612
1830
|
h('g', null, leftLabels),
|
|
1613
1831
|
h('g', null, rightLabels),
|
|
@@ -1737,12 +1955,14 @@ window.__ModuleLoader__.load({
|
|
|
1737
1955
|
}
|
|
1738
1956
|
// 还没有任何数字时显示占位符,而不是误导性的 0。
|
|
1739
1957
|
const pending = bucket === null && state.turnsState !== 'ready'
|
|
1958
|
+
// 计价兜底模型:会话日志里查不到模型时用它(设置页下拉选的那个 → 账号当前模型 → 默认)。
|
|
1740
1959
|
const model = bucketModel
|
|
1960
|
+
|| state.modelFilter
|
|
1741
1961
|
|| (state.account && typeof state.account.model === 'string' && state.account.model ? state.account.model : DEFAULT_MODEL)
|
|
1742
1962
|
const at = bucketTime > 0 ? bucketTime : Date.now()
|
|
1743
1963
|
const peak = isPeakAt(at)
|
|
1744
1964
|
const tokens = bucket === null ? 0 : bucket.hit + bucket.miss + bucket.out
|
|
1745
|
-
const cost = bucket === null ? 0 : costOf(bucket, priceFor(
|
|
1965
|
+
const cost = bucket === null ? 0 : costOf(bucket, priceFor(bucketModel, state.modelFilter).table, peak)
|
|
1746
1966
|
|
|
1747
1967
|
// 账户余额:成功 → ¥金额;读取中 → …;失败/未配置 → —(原因放 title,不挤占胶囊宽度)。
|
|
1748
1968
|
const balance = state.balance
|
|
@@ -1807,13 +2027,26 @@ window.__ModuleLoader__.load({
|
|
|
1807
2027
|
|
|
1808
2028
|
const now = Date.now()
|
|
1809
2029
|
const surface = modalSurface(state.modalStyle, state.modalImageUrl)
|
|
1810
|
-
const
|
|
2030
|
+
const accountModel = state.account && typeof state.account.model === 'string' && state.account.model ? state.account.model : DEFAULT_MODEL
|
|
2031
|
+
// 面板里的「模型」下拉 = 计价兜底模型(日志查不到模型时用它)+ 图例口径。
|
|
2032
|
+
const model = state.modelFilter || accountModel
|
|
1811
2033
|
const price = priceFor(model)
|
|
1812
2034
|
const peak = isPeakAt(now)
|
|
1813
|
-
const
|
|
1814
|
-
|
|
2035
|
+
const allSeries = buildSeries(state.turns, model, now)
|
|
2036
|
+
// 只看选中模型时,卡片/图表都按该模型过滤(选「全部模型」时不过滤)。
|
|
2037
|
+
const series = state.modelFilter ? allSeries.filter(function (s) { return s.model === state.modelFilter }) : allSeries
|
|
2038
|
+
const aggregated = { hit: 0, miss: 0, out: 0 }
|
|
2039
|
+
let totalCost = 0
|
|
2040
|
+
for (let i = 0; i < series.length; i += 1) {
|
|
2041
|
+
aggregated.hit += series[i].hit
|
|
2042
|
+
aggregated.miss += series[i].miss
|
|
2043
|
+
aggregated.out += series[i].out
|
|
2044
|
+
totalCost += series[i].cost
|
|
2045
|
+
}
|
|
1815
2046
|
const tokens = aggregated.hit + aggregated.miss + aggregated.out
|
|
1816
|
-
const
|
|
2047
|
+
const modelSet = {}
|
|
2048
|
+
for (let i = 0; i < allSeries.length; i += 1) modelSet[allSeries[i].model] = allSeries[i]
|
|
2049
|
+
const modelsUsed = Object.keys(modelSet).map(function (k) { return modelSet[k] })
|
|
1817
2050
|
const balance = state.balance
|
|
1818
2051
|
const loading = state.balanceState === 'loading'
|
|
1819
2052
|
const balanceErr = balanceErrorText()
|
|
@@ -1837,6 +2070,13 @@ window.__ModuleLoader__.load({
|
|
|
1837
2070
|
if (typeof balance.granted === 'number') parts.push(tt('赠送 {amount}', { amount: moneyBalance(balance.granted, balance.currency) }))
|
|
1838
2071
|
if (typeof balance.toppedUp === 'number') parts.push(tt('充值 {amount}', { amount: moneyBalance(balance.toppedUp, balance.currency) }))
|
|
1839
2072
|
balanceHint = parts.length ? parts.join(' · ') : tt('DeepSeek 账户余额')
|
|
2073
|
+
// 选了模型时,把余额换算成「按该模型还能跑多少 token」——这就是余额显示与模型的联动。
|
|
2074
|
+
const perMillion = price.table.hit * 0.5 + price.table.miss * 0.3 + price.table.out * 0.2
|
|
2075
|
+
if (typeof balance.total === 'number' && perMillion > 0 && balance.currency === 'CNY') {
|
|
2076
|
+
const affordable = (balance.total / perMillion) * 1e6
|
|
2077
|
+
balanceHint = (parts.length ? parts.join(' · ') + ' · ' : '')
|
|
2078
|
+
+ tt('按 {model} 约还可跑 {tokens} tokens', { model: price.label, tokens: fmtTokens(affordable) })
|
|
2079
|
+
}
|
|
1840
2080
|
}
|
|
1841
2081
|
|
|
1842
2082
|
function close() {
|
|
@@ -1870,11 +2110,31 @@ window.__ModuleLoader__.load({
|
|
|
1870
2110
|
h('div', { className: 'dcb-head' },
|
|
1871
2111
|
h('div', { className: 'dcb-head-title' }, tt('⛽ 本会话消耗')),
|
|
1872
2112
|
h('div', { className: 'dcb-head-actions' },
|
|
2113
|
+
// 模型下拉:只看某个模型,同时作为「查不到模型时」的计价基准。
|
|
2114
|
+
h('select', {
|
|
2115
|
+
className: 'dcb-select dcb-select-inline',
|
|
2116
|
+
'aria-label': tt('模型'),
|
|
2117
|
+
title: tt('按模型筛选;同时作为计价兜底'),
|
|
2118
|
+
value: state.modelFilter,
|
|
2119
|
+
onChange: function (event) { store.setModelFilter(event.target.value) },
|
|
2120
|
+
},
|
|
2121
|
+
h('option', { key: 'all', value: '' }, tt('全部模型')),
|
|
2122
|
+
modelOptionNodes(modelsUsed.length > 0
|
|
2123
|
+
? modelsUsed.map(function (m) { return MODEL_BY_ID[m.model] || { id: m.model, label: m.label, provider: '—' } })
|
|
2124
|
+
: MODELS)),
|
|
1873
2125
|
h('button', { type: 'button', className: 'dcb-btn', onClick: function () { store.ensureBalance(true) } }, tt('刷新余额')),
|
|
1874
2126
|
h('button', { type: 'button', className: 'dcb-btn dcb-btn-close', onClick: close, title: tt('Esc 关闭') }, '✕'))),
|
|
1875
2127
|
h('div', { className: 'dcb-cards' },
|
|
1876
2128
|
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, {
|
|
2129
|
+
h(Card, {
|
|
2130
|
+
label: tt('估算金额'),
|
|
2131
|
+
value: moneyApprox(totalCost),
|
|
2132
|
+
hint: (state.modelFilter
|
|
2133
|
+
? tt('仅 {model}', { model: modelDetail(state.modelFilter) })
|
|
2134
|
+
: tt('{count} 个模型各自计价', { count: String(modelsUsed.length || 1) })
|
|
2135
|
+
+ (modelsUsed.length === 1 ? ' · ' + modelDetail(modelsUsed[0].model) : ''))
|
|
2136
|
+
+ (price.peakPricing ? tt(peak ? ' · 峰时 ×2' : ' · 谷时') : ''),
|
|
2137
|
+
}),
|
|
1878
2138
|
h(Card, { label: tt('当前时段'), value: tt(peak ? '高峰' : '空闲'), hint: tt('北京时间 工作日 09:00-12:00 / 14:00-18:00') }),
|
|
1879
2139
|
h('div', { className: 'dcb-card' },
|
|
1880
2140
|
h('div', { className: 'dcb-card-label' }, tt('账户余额')),
|
|
@@ -1884,7 +2144,7 @@ window.__ModuleLoader__.load({
|
|
|
1884
2144
|
h(UsageChart, { series: series }),
|
|
1885
2145
|
h('div', { className: 'dcb-foot' },
|
|
1886
2146
|
state.turnsError ? h('span', { className: 'dcb-foot-warn' }, turnsErrorText() + ' ') : null,
|
|
1887
|
-
tt('
|
|
2147
|
+
tt('金额按各模型官方价估算(DeepSeek 区分峰谷 ×1/×2,其它厂商按 USD 价 ×7.1 折人民币),仅供参考;数据源为本会话日志,刷新后自动恢复。'))))
|
|
1888
2148
|
}
|
|
1889
2149
|
|
|
1890
2150
|
/* ===================== settings section: history page ===================== */
|
|
@@ -1999,9 +2259,11 @@ window.__ModuleLoader__.load({
|
|
|
1999
2259
|
}
|
|
2000
2260
|
|
|
2001
2261
|
/**
|
|
2002
|
-
* 纯聚合:时间过滤 →
|
|
2262
|
+
* 纯聚合:时间过滤 → 工作区/模型过滤 → 按小时/日/月分桶(空桶补 0)→ 峰谷与金额拆分 →
|
|
2263
|
+
* 每个桶再按模型拆一份 `byModel`(折线图一个模型一条线就靠它)。
|
|
2003
2264
|
* 刻意自包含(只读 input,不引用闭包变量),所以能直接 String() 进 Web Worker。
|
|
2004
|
-
* @param input {records, from, to, wid, granularity: 'hour'|'day'|'month', prices, holidays, windows}
|
|
2265
|
+
* @param input {records, from, to, wid, model, granularity: 'hour'|'day'|'month', prices, holidays, windows}
|
|
2266
|
+
* `model` 非空时只统计该模型;`prices[id]` 里的 `peak: true` 表示该模型参与峰谷计时。
|
|
2005
2267
|
*/
|
|
2006
2268
|
function computeConsumptionStats(input) {
|
|
2007
2269
|
const records = input.records || []
|
|
@@ -2011,6 +2273,7 @@ window.__ModuleLoader__.load({
|
|
|
2011
2273
|
const from = typeof input.from === 'number' ? input.from : 0
|
|
2012
2274
|
const to = typeof input.to === 'number' ? input.to : 0
|
|
2013
2275
|
const wid = typeof input.wid === 'string' ? input.wid : ''
|
|
2276
|
+
const modelFilter = typeof input.model === 'string' ? input.model : ''
|
|
2014
2277
|
const granularity = input.granularity === 'month' ? 'month' : (input.granularity === 'hour' ? 'hour' : 'day')
|
|
2015
2278
|
// noFill:只要合计不要时间轴(设置页顶部的“累计”卡片用),跳过分桶补齐。
|
|
2016
2279
|
const noFill = input.noFill === true
|
|
@@ -2042,7 +2305,10 @@ window.__ModuleLoader__.load({
|
|
|
2042
2305
|
function priceOf(model) {
|
|
2043
2306
|
const id = typeof model === 'string' ? model : ''
|
|
2044
2307
|
if (prices[id]) return prices[id]
|
|
2045
|
-
|
|
2308
|
+
// 兼容历史 id:pro/opus/max 归到更贵的一档,其余按 flash。
|
|
2309
|
+
if (id.toLowerCase().indexOf('pro') >= 0 || id.toLowerCase().indexOf('opus') >= 0 || id.toLowerCase().indexOf('max') >= 0) {
|
|
2310
|
+
return prices['deepseek-v4-pro'] || prices['deepseek-flash']
|
|
2311
|
+
}
|
|
2046
2312
|
return prices['deepseek-flash']
|
|
2047
2313
|
}
|
|
2048
2314
|
function keyOf(ms) {
|
|
@@ -2070,14 +2336,17 @@ window.__ModuleLoader__.load({
|
|
|
2070
2336
|
const produced = n(r.out)
|
|
2071
2337
|
const tokens = hit + miss + produced
|
|
2072
2338
|
if (tokens === 0) continue
|
|
2339
|
+
const rowModel = typeof r.model === 'string' && r.model ? r.model : ''
|
|
2340
|
+
if (modelFilter && rowModel !== modelFilter) continue
|
|
2073
2341
|
const peak = isPeak(t)
|
|
2074
|
-
const table = priceOf(
|
|
2075
|
-
|
|
2342
|
+
const table = priceOf(rowModel)
|
|
2343
|
+
// 只有 `peak: true` 的模型(DeepSeek)才乘峰时系数。
|
|
2344
|
+
const k = peak && table.peak === true ? 2 : 1
|
|
2076
2345
|
const cost = ((hit / 1e6) * table.hit + (miss / 1e6) * table.miss + (produced / 1e6) * table.out) * k
|
|
2077
2346
|
const key = keyOf(t)
|
|
2078
2347
|
let b = buckets[key]
|
|
2079
2348
|
if (!b) {
|
|
2080
|
-
b = { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0 }
|
|
2349
|
+
b = { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0, byModel: {} }
|
|
2081
2350
|
buckets[key] = b
|
|
2082
2351
|
}
|
|
2083
2352
|
b.tokens += tokens
|
|
@@ -2101,13 +2370,22 @@ window.__ModuleLoader__.load({
|
|
|
2101
2370
|
}
|
|
2102
2371
|
const model = typeof r.model === 'string' && r.model ? r.model : 'unknown'
|
|
2103
2372
|
if (b.models.indexOf(model) < 0) b.models.push(model)
|
|
2373
|
+
// 桶内按模型拆分:cost/tokens 给折线图每个模型一条线用。
|
|
2374
|
+
let slot = b.byModel[model]
|
|
2375
|
+
if (!slot) {
|
|
2376
|
+
slot = { cost: 0, tokens: 0, records: 0 }
|
|
2377
|
+
b.byModel[model] = slot
|
|
2378
|
+
}
|
|
2379
|
+
slot.cost += cost
|
|
2380
|
+
slot.tokens += tokens
|
|
2381
|
+
slot.records += 1
|
|
2104
2382
|
count += 1
|
|
2105
2383
|
}
|
|
2106
2384
|
|
|
2107
2385
|
const points = []
|
|
2108
2386
|
function push(key, ms) {
|
|
2109
2387
|
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 }
|
|
2388
|
+
const point = b || { key: key, tokens: 0, cost: 0, peakTokens: 0, peakCost: 0, valleyTokens: 0, valleyCost: 0, models: [], records: 0, byModel: {} }
|
|
2111
2389
|
if (granularity === 'month') point.label = key
|
|
2112
2390
|
else if (granularity === 'hour') point.label = key.slice(11) + ':00'
|
|
2113
2391
|
else point.label = key.slice(5)
|
|
@@ -2244,10 +2522,8 @@ window.__ModuleLoader__.load({
|
|
|
2244
2522
|
function yToken(v) { return HIST_PAD.t + innerH - (v / topToken) * innerH }
|
|
2245
2523
|
function yCost(v) { return HIST_PAD.t + innerH - (v / topCost) * innerH }
|
|
2246
2524
|
|
|
2247
|
-
const costPts = []
|
|
2248
2525
|
const tokenPts = []
|
|
2249
2526
|
for (let i = 0; i < n; i += 1) {
|
|
2250
|
-
costPts.push(xAt(i).toFixed(2) + ',' + yCost(points[i].cost).toFixed(2))
|
|
2251
2527
|
tokenPts.push(xAt(i).toFixed(2) + ',' + yToken(points[i].tokens).toFixed(2))
|
|
2252
2528
|
}
|
|
2253
2529
|
|
|
@@ -2272,13 +2548,47 @@ window.__ModuleLoader__.load({
|
|
|
2272
2548
|
}
|
|
2273
2549
|
|
|
2274
2550
|
const dots = []
|
|
2551
|
+
// 每个模型一条金额折线(各自颜色):该桶里没有这个模型就断线。
|
|
2552
|
+
const modelUsage = {}
|
|
2553
|
+
const modelOrder = []
|
|
2275
2554
|
for (let i = 0; i < n; i += 1) {
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2555
|
+
const byModel = points[i].byModel || {}
|
|
2556
|
+
for (const key in byModel) {
|
|
2557
|
+
if (!Object.prototype.hasOwnProperty.call(byModel, key)) continue
|
|
2558
|
+
if (modelUsage[key] === undefined) {
|
|
2559
|
+
modelUsage[key] = []
|
|
2560
|
+
modelOrder.push(key)
|
|
2561
|
+
}
|
|
2562
|
+
modelUsage[key].push(i)
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const modelLines = []
|
|
2566
|
+
for (let m = 0; m < modelOrder.length; m += 1) {
|
|
2567
|
+
const key = modelOrder[m]
|
|
2568
|
+
const rows = modelUsage[key]
|
|
2569
|
+
const color = modelColor(key)
|
|
2570
|
+
let d = ''
|
|
2571
|
+
for (let k = 0; k < rows.length; k += 1) {
|
|
2572
|
+
const idx = rows[k]
|
|
2573
|
+
const seg = points[idx].byModel[key]
|
|
2574
|
+
d += (k === 0 || rows[k - 1] !== idx - 1 ? 'M' : 'L') + xAt(idx).toFixed(2) + ' ' + yCost(seg.cost).toFixed(2) + ' '
|
|
2575
|
+
}
|
|
2576
|
+
modelLines.push(h('path', { key: 'hml' + key, d: d.trim(), className: 'dcb-line dcb-line-cost', stroke: color, fill: 'none' }))
|
|
2577
|
+
for (let k = 0; k < rows.length; k += 1) {
|
|
2578
|
+
const idx = rows[k]
|
|
2579
|
+
dots.push(h('circle', {
|
|
2580
|
+
key: 'hc' + key + idx,
|
|
2581
|
+
cx: xAt(idx),
|
|
2582
|
+
cy: yCost(points[idx].byModel[key].cost),
|
|
2583
|
+
r: hover === idx ? 4 : 2.2,
|
|
2584
|
+
className: 'dcb-dot dcb-dot-cost',
|
|
2585
|
+
fill: color,
|
|
2586
|
+
}))
|
|
2279
2587
|
}
|
|
2588
|
+
}
|
|
2589
|
+
for (let i = 0; i < n; i += 1) {
|
|
2280
2590
|
if (showTokens && points[i].records > 0) {
|
|
2281
|
-
dots.push(h('circle', { key: 'ht' + i, cx: xAt(i), cy: yToken(points[i].tokens), r:
|
|
2591
|
+
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
2592
|
}
|
|
2283
2593
|
}
|
|
2284
2594
|
|
|
@@ -2307,7 +2617,9 @@ window.__ModuleLoader__.load({
|
|
|
2307
2617
|
h('div', { className: 'dcb-tooltip-row' }, h('span', null, 'Token'), h('b', null, fmtTokens(hovered.tokens))),
|
|
2308
2618
|
h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('峰时')), h('b', null, moneyApprox(hovered.peakCost) + ' / ' + fmtTokens(hovered.peakTokens))),
|
|
2309
2619
|
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
|
|
2620
|
+
h('div', { className: 'dcb-tooltip-row' }, h('span', null, tt('模型')), h('b', null, hovered.models.length
|
|
2621
|
+
? hovered.models.map(function (m) { return modelLabel(m) }).join(', ')
|
|
2622
|
+
: '—')))
|
|
2311
2623
|
: null
|
|
2312
2624
|
|
|
2313
2625
|
return h('div', { className: 'dcb-chart-body' },
|
|
@@ -2320,8 +2632,8 @@ window.__ModuleLoader__.load({
|
|
|
2320
2632
|
},
|
|
2321
2633
|
h('g', null, grid),
|
|
2322
2634
|
crosshair,
|
|
2323
|
-
showTokens ? h('polyline', { className: 'dcb-line', points: tokenPts.join(' ') }) : null,
|
|
2324
|
-
h('
|
|
2635
|
+
showTokens ? h('polyline', { className: 'dcb-line dcb-line-token', points: tokenPts.join(' ') }) : null,
|
|
2636
|
+
h('g', null, modelLines),
|
|
2325
2637
|
h('g', null, dots),
|
|
2326
2638
|
h('g', null, leftLabels),
|
|
2327
2639
|
h('g', null, rightLabels),
|
|
@@ -2358,6 +2670,8 @@ window.__ModuleLoader__.load({
|
|
|
2358
2670
|
const s8 = React.useState(null)
|
|
2359
2671
|
const allStats = s8[0]
|
|
2360
2672
|
const setAllStats = s8[1]
|
|
2673
|
+
// 「模型」下拉的状态放在 store 里(弹层与设置页共享同一个选择)。
|
|
2674
|
+
const modelFilter = store.state.modelFilter
|
|
2361
2675
|
const s9 = React.useState(0)
|
|
2362
2676
|
const setStoreTick = s9[1]
|
|
2363
2677
|
|
|
@@ -2408,6 +2722,7 @@ window.__ModuleLoader__.load({
|
|
|
2408
2722
|
from: range.from,
|
|
2409
2723
|
to: range.to,
|
|
2410
2724
|
wid: wid,
|
|
2725
|
+
model: modelFilter,
|
|
2411
2726
|
granularity: range.granularity,
|
|
2412
2727
|
prices: PRICES,
|
|
2413
2728
|
holidays: CN_FIXED_HOLIDAYS,
|
|
@@ -2423,7 +2738,7 @@ window.__ModuleLoader__.load({
|
|
|
2423
2738
|
setStats(fallback)
|
|
2424
2739
|
})
|
|
2425
2740
|
return undefined
|
|
2426
|
-
}, [records, rangeKey, wid, calFrom, calTo, wsSig])
|
|
2741
|
+
}, [records, rangeKey, wid, calFrom, calTo, wsSig, modelFilter])
|
|
2427
2742
|
|
|
2428
2743
|
// 顶部“累计”卡片:全部历史合计(不受筛选影响),只算合计不铺时间轴。
|
|
2429
2744
|
React.useEffect(function () {
|
|
@@ -2480,7 +2795,8 @@ window.__ModuleLoader__.load({
|
|
|
2480
2795
|
rateNote = tt('金额按人民币报价折算显示')
|
|
2481
2796
|
}
|
|
2482
2797
|
const accountModel = st.account && typeof st.account.model === 'string' && st.account.model ? st.account.model : DEFAULT_MODEL
|
|
2483
|
-
|
|
2798
|
+
// 「模型」下拉选了具体模型时,它同时是计价兜底(日志里查不到模型时用它)。
|
|
2799
|
+
const accountPrice = priceFor(modelFilter || accountModel)
|
|
2484
2800
|
const nowPeak = isPeakAt(Date.now())
|
|
2485
2801
|
const balance = st.balance
|
|
2486
2802
|
const balanceLoading = st.balanceState === 'loading'
|
|
@@ -2510,6 +2826,15 @@ window.__ModuleLoader__.load({
|
|
|
2510
2826
|
const index = buildWorkspaceIndex(wsSig)
|
|
2511
2827
|
const points = stats ? stats.points : []
|
|
2512
2828
|
const hasData = !!stats && stats.count > 0
|
|
2829
|
+
// 图例:这一区间里出现过的模型(按聚合结果的 byModel 收集,颜色与折线一致)。
|
|
2830
|
+
const legendModels = []
|
|
2831
|
+
for (let pi = 0; pi < points.length; pi += 1) {
|
|
2832
|
+
const byModel = points[pi].byModel || {}
|
|
2833
|
+
for (const key in byModel) {
|
|
2834
|
+
if (!Object.prototype.hasOwnProperty.call(byModel, key)) continue
|
|
2835
|
+
if (legendModels.indexOf(key) < 0) legendModels.push(key)
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2513
2838
|
const rangeOptions = [
|
|
2514
2839
|
{ id: 'today', label: tt('今天') },
|
|
2515
2840
|
{ id: 'week', label: tt('近一周') },
|
|
@@ -2533,7 +2858,7 @@ window.__ModuleLoader__.load({
|
|
|
2533
2858
|
h(Card, {
|
|
2534
2859
|
label: tt('估算金额'),
|
|
2535
2860
|
value: allStats ? moneyApprox(allStats.totalCost) : '—',
|
|
2536
|
-
hint: (accountPrice.known ?
|
|
2861
|
+
hint: (accountPrice.known ? modelDetail(modelFilter || accountModel) : tt('{id}(按 flash 估算)', { id: accountPrice.id })) + tt(' · 全部历史逐轮计价'),
|
|
2537
2862
|
}),
|
|
2538
2863
|
h(Card, {
|
|
2539
2864
|
label: tt('当前时段'),
|
|
@@ -2592,6 +2917,17 @@ window.__ModuleLoader__.load({
|
|
|
2592
2917
|
h('div', { className: 'dcb-field' },
|
|
2593
2918
|
h('span', { className: 'dcb-field-label' }, tt('数据')),
|
|
2594
2919
|
h('button', { type: 'button', className: 'dcb-btn', onClick: function () { setReloadTick(function (v) { return v + 1 }) } }, tt('刷新'))),
|
|
2920
|
+
// 模型筛选:只看某个模型的消耗;同时作为「日志查不到模型」时的计价基准。
|
|
2921
|
+
h('div', { className: 'dcb-field' },
|
|
2922
|
+
h('label', { className: 'dcb-field-label', htmlFor: 'dcb-model-select' }, tt('模型')),
|
|
2923
|
+
h('select', {
|
|
2924
|
+
id: 'dcb-model-select',
|
|
2925
|
+
className: 'dcb-select',
|
|
2926
|
+
value: modelFilter,
|
|
2927
|
+
onChange: function (event) { store.setModelFilter(event.target.value) },
|
|
2928
|
+
},
|
|
2929
|
+
h('option', { key: 'all', value: '' }, tt('全部模型')),
|
|
2930
|
+
modelOptionNodes(MODELS))),
|
|
2595
2931
|
// 显示币种:报价表永远是人民币,这里只决定展示时换算成什么货币。
|
|
2596
2932
|
h('div', { className: 'dcb-field' },
|
|
2597
2933
|
h('label', { className: 'dcb-field-label', htmlFor: 'dcb-currency-select' }, tt('货币')),
|
|
@@ -2607,7 +2943,14 @@ window.__ModuleLoader__.load({
|
|
|
2607
2943
|
h('div', { className: 'dcb-set-note dcb-rate-note' }, rateNote),
|
|
2608
2944
|
h('div', { className: 'dcb-set-body' },
|
|
2609
2945
|
h('div', { className: 'dcb-hist-legend' },
|
|
2610
|
-
|
|
2946
|
+
// 每个模型一枚颜色 chip(与折线同色),右侧是 Token 轴开关。
|
|
2947
|
+
legendModels.length > 0
|
|
2948
|
+
? legendModels.map(function (key) {
|
|
2949
|
+
return h('span', { key: 'lg' + key, className: 'dcb-legend-chip', title: modelDetail(key) },
|
|
2950
|
+
h('i', { className: 'dcb-legend-dot', style: { background: modelColor(key) } }),
|
|
2951
|
+
modelLabel(key))
|
|
2952
|
+
})
|
|
2953
|
+
: h('span', { className: 'dcb-tog dcb-tog-cost is-on' }, h('i', null), tt('估算金额(右轴)')),
|
|
2611
2954
|
h('button', {
|
|
2612
2955
|
type: 'button',
|
|
2613
2956
|
className: showTokens ? 'dcb-tog is-on' : 'dcb-tog',
|
|
@@ -3068,6 +3411,14 @@ window.__ModuleLoader__.load({
|
|
|
3068
3411
|
.dcb-legend-item{display:inline-flex;align-items:center;gap:6px}
|
|
3069
3412
|
.dcb-legend-dot{display:inline-block;width:14px;height:3px;border-radius:2px;background:var(--dsw-alias-brand-primary)}
|
|
3070
3413
|
.dcb-legend-dot.dcb-dot-cost{background:var(--dsw-alias-state-success-primary)}
|
|
3414
|
+
/* 图例里的模型 chip(与折线同色) */
|
|
3415
|
+
.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)}
|
|
3416
|
+
.dcb-legend-chip .dcb-legend-dot{width:9px;height:9px;border-radius:50%}
|
|
3417
|
+
/* 弹层标题栏里的模型下拉 */
|
|
3418
|
+
.dcb-select-inline{height:22px;font-size:11px;min-width:118px;max-width:190px;padding:0 6px;border-radius:6px}
|
|
3419
|
+
.dcb-select-inline:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}
|
|
3420
|
+
/* tooltip 里的模型色点 */
|
|
3421
|
+
.dcb-model-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:middle}
|
|
3071
3422
|
.dcb-chart-body{position:relative}
|
|
3072
3423
|
.dcb-chart-svg{display:block;width:100%;height:216px}
|
|
3073
3424
|
.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.
|
|
3
|
+
"version": "1.3.2",
|
|
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",
|