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.
- package/README.md +55 -6
- package/lib/client.js +472 -56
- 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
|
|
@@ -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` (
|
|
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
|
-
|
|
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,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,
|
|
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,
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1781
|
+
// 每个模型一条金额折线(各自颜色);该模型没有数据的轮次断开,不做插值连线。
|
|
1782
|
+
const byModel = {}
|
|
1783
|
+
const modelOrder = []
|
|
1561
1784
|
for (let i = 0; i < n; i += 1) {
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
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'
|
|
1587
|
-
|
|
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
|
-
|
|
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('
|
|
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(
|
|
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
|
|
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
|
|
1814
|
-
|
|
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
|
|
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, {
|
|
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('
|
|
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
|
-
* 纯聚合:时间过滤 →
|
|
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
|
-
|
|
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(
|
|
2075
|
-
|
|
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
|
|
2277
|
-
|
|
2278
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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.
|
|
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",
|