@goodandready/dsh-cost-meter 0.8.1 → 0.8.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 +18 -2
- package/README.ru.md +156 -0
- package/README.zh.md +144 -0
- package/docs/README.ru.md +17 -1
- package/docs/README.zh.md +17 -1
- package/docs/design/DESIGN.md +6 -0
- package/docs/plans/findings.md +10 -14
- package/docs/plans/progress.md +10 -10
- package/docs/plans/task_plan.md +27 -27
- package/lib/client.js +219 -147
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,10 +18,20 @@
|
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
20
|
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
-
<a href="
|
|
22
|
-
<a href="
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -145,3 +155,9 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
145
155
|
- **Manual Catalog Sync**: On-demand catalog fetch via `POST /dsh-cost-meter/refresh` or UI "Sync Now" button.
|
|
146
156
|
- **Savings Advice & Budget Threshold**: Popover hints on impending off-peak discounts (50% off) and optional warning thresholds (`budgetThreshold`).
|
|
147
157
|
- **Quick Summary Export**: Instant markdown/text summary copy to clipboard.
|
|
158
|
+
|
|
159
|
+
### UI Refinements & Language Standards (v0.8.2)
|
|
160
|
+
- **Native Dot Status Indicator**: Header chip uses a refined status dot (green for off-peak, amber for peak, pulsing red for budget overrun, neutral for flat/idle) rather than full-fill backgrounds.
|
|
161
|
+
- **Context Cache Savings**: Automatically calculates and highlights financial savings gained from prompt caching (e.g. `Cache saved: ≈ $0.14 (-68%)`).
|
|
162
|
+
- **Multi-Model Accordion**: Clean collapsible list when 3 or more models are used in a single session.
|
|
163
|
+
- **DSH Locale Standards**: Full English (`en`) and Chinese (`zh`) locale dictionaries registered directly in the client bundle. Russian translations are provided externally via `@goodandready/dsh-russian-lang`.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-cost-meter
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Чип стоимости сессии, переключатель пиковых тарифов и мониторинг расходов токенов для DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-cost-meter"><img src="https://img.shields.io/npm/v/@goodandready/dsh-cost-meter.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="../LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-cost-meter.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- Обязательная кнопка перехода на витрину всех проектов -->
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ⚡ Обзор и решаемая проблема
|
|
40
|
+
|
|
41
|
+
Разработка с помощью ИИ-агентов и генерация кода требуют активного потребления токенов (промпты, рассуждения, контекстный кэш). Без непрерывного финансового контроля разработчики рискуют столкнуться с перерасходом бюджета или упустить окна 50% скидок внепикового тарифа.
|
|
42
|
+
|
|
43
|
+
**`@goodandready/dsh-cost-meter`** встраивает компактный высокоточный телеметрический чип прямо в заголовок диалога DeepSeek Harness (`conversation.session.header.utilities`):
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
● ≈ ₽8.55 0:17 ← Чип стоимости сессии в шапке с обратным отсчётом тарифа
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
По клику открывается интерактивная панель: тарифная сетка за 1M токенов (не-пик / пик с подсветкой активного режима), статус окон в UTC и детальная разбивка расходов текущей сессии по моделям.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🏛️ Архитектура
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD
|
|
57
|
+
subgraph StreamTelemetry ["Рантайм DeepSeek Harness"]
|
|
58
|
+
Req["Запрос LLM / Заголовок<br/>(provider, model)"]
|
|
59
|
+
StreamHook["Потоковые инкрементальные чанки"]
|
|
60
|
+
Proj["Проекция costByModel<br/>(30-минутные слоты UTC)"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
subgraph PricingCatalog ["Движок тарификации"]
|
|
64
|
+
Manual["Пользовательские цены<br/>(settings.yaml: prices)"]
|
|
65
|
+
DeepSeekTier["Сетка DeepSeek<br/>(Пик / 50% скидка не-пик)"]
|
|
66
|
+
OpenRouterTier["Каталог OpenRouter<br/>(Ежедневный кэш)"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph UI ["Интерфейс пользователя"]
|
|
70
|
+
Chip["Чип стоимости в шапке<br/>(Тариф + таймер смены)"]
|
|
71
|
+
Modal["Модальное окно разбивки<br/>(Ставки за 1M, окна UTC, модели)"]
|
|
72
|
+
Settings["Карточка настроек<br/>(Валюта, курс USD, таймзона)"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Req --> Proj
|
|
76
|
+
StreamHook --> Proj
|
|
77
|
+
Proj --> Chip
|
|
78
|
+
PricingCatalog --> Proj
|
|
79
|
+
Manual --> PricingCatalog
|
|
80
|
+
DeepSeekTier --> PricingCatalog
|
|
81
|
+
OpenRouterTier --> PricingCatalog
|
|
82
|
+
Chip --> Modal
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ✨ Ключевые возможности
|
|
88
|
+
|
|
89
|
+
1. **Живой расчёт во время стриминга**: расчёт стоимости инкрементально обновляется в реальном времени прямо во время генерации ответа.
|
|
90
|
+
2. **Двухуровневая сетка тарифов**: официальные пиковые окна DeepSeek (01:00–04:00 и 06:00–10:00 UTC) со скидкой 50% во внепиковое время.
|
|
91
|
+
3. **Иммутабельность получасовых слотов UTC**: исторические расходы сессии фиксируются по ставке слота и не пересчитываются задним числом при смене тарифа.
|
|
92
|
+
4. **Provider-aware тарификация**: раздельный учёт для прямых подключений DeepSeek и шлюзов OpenRouter.
|
|
93
|
+
5. **Гибкая настройка валюты**: поддержка пользовательских символов валют (`₽`, `$`, `€`, `¥`), курсов конвертации и таймзон.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📦 Установка
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dsh plugin --profile web add @goodandready/dsh-cost-meter
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Перезапустите экземпляр DeepSeek Harness и обновите страницу в браузере.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ⚙️ Таблица конфигурации (`settings.yaml`)
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
dsh-cost-meter:
|
|
111
|
+
currency: "₽"
|
|
112
|
+
usdRate: 90.0
|
|
113
|
+
displayTimeZone: "Europe/Moscow"
|
|
114
|
+
useOpenRouter: true
|
|
115
|
+
prices: {}
|
|
116
|
+
modelMap: {}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Параметры конфигурации
|
|
120
|
+
|
|
121
|
+
| Параметр | Доступность | Тип | По умолчанию | Описание |
|
|
122
|
+
|:---|:---|:---|:---|:---|
|
|
123
|
+
| `currency` | GUI / `settings.yaml` | `string` | `"₽"` | Символ валюты в интерфейсе (например, `₽`, `$`, `€`) |
|
|
124
|
+
| `usdRate` | GUI / `settings.yaml` | `number` | `80.0` | Курс обмена: единиц валюты за 1 USD |
|
|
125
|
+
| `displayTimeZone` | GUI / `settings.yaml` | `string` | `"Europe/Moscow"` | IANA часовой пояс для отображения окон пика и не-пика |
|
|
126
|
+
| `useOpenRouter` | GUI / `settings.yaml` | `boolean` | `true` | Загружать ли каталог тарифов и скидочных окон OpenRouter |
|
|
127
|
+
| `refreshHours` | GUI / `settings.yaml` | `number` | `24` | Частота обновления каталога OpenRouter (в часах) |
|
|
128
|
+
| `deepseekPeakPrices` | Только `settings.yaml` | `object` | `{}` | Переопределение встроенных пиковых ставок DeepSeek по id модели |
|
|
129
|
+
| `prices` | Только `settings.yaml` | `object` | `{}` | Ручные ставки за 1M токенов `{ input, output, cacheHit?, cacheWrite? }` |
|
|
130
|
+
| `modelMap` | Только `settings.yaml` | `object` | `{}` | Сопоставление маршрута `"provider/model"` на id в OpenRouter |
|
|
131
|
+
| `manualPeakWindowsUtc` | Только `settings.yaml` | `array` | `[]` | Ручные окна пика HH:MM-HH:MM UTC для ручных ставок |
|
|
132
|
+
| `manualOffPeakMultiplier` | Только `settings.yaml` | `number` | `1.0` | Коэффициент тарифа вне ручных окон пика |
|
|
133
|
+
|
|
134
|
+
> **Интерфейс настроек (GUI) vs settings.yaml**: Основные параметры (`currency`, `usdRate`, `displayTimeZone`, `useOpenRouter`, `refreshHours`) настраиваются прямо в карточке интерфейса DSH (**Настройки → Плагины → Настройки плагинов → Cost Meter**). Расширенные структурированные правила сопоставления и ручных тарифов задаются через `settings.yaml` из-за сложной древовидной структуры.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🧪 Тестирование
|
|
139
|
+
|
|
140
|
+
Запуск набора юнит-тестов:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📄 Лицензия
|
|
149
|
+
|
|
150
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
151
|
+
|
|
152
|
+
### Производительность и интеллектуальные подсказки (v0.8.1)
|
|
153
|
+
- **Алгоритм O(K)**: Моментальный расчёт границ смены тарифов и мемоизация экстремумов без лишних циклов в браузере.
|
|
154
|
+
- **Серверный кэш и ETag**: Поддержка заголовка `304 Not Modified` для маршрута `/state` и кэширование сопоставлений моделей.
|
|
155
|
+
- **Ручная синхронизация**: Кнопка «Синхронизировать сейчас» в настройках с защитой от флуда (`POST /dsh-cost-meter/refresh`).
|
|
156
|
+
- **Подсказки экономии и порог бюджета**: Отсчёт времени до льготного окна (скидка 50%), порог сессии (`budgetThreshold`) и экспорт сводки в буфер обмена.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-cost-meter
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>面向 DeepSeek Harness 的会话实时花费计费芯片、波峰/波谷阶梯电价与 Token 监控插件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-cost-meter"><img src="https://img.shields.io/npm/v/@goodandready/dsh-cost-meter.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="../LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-cost-meter.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- 作者所有项目展示页面链接 -->
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者所有开源项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="所有项目"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ⚡ 概述与核心痛点
|
|
40
|
+
|
|
41
|
+
在使用智能体与大语言模型编程时,模型推理、长上下文以及缓存读写会产生大量 Token 消耗。如果缺乏实时的财务监控,开发者容易错过官方 50% 闲时折扣窗口,或因死循环导致账单超出预算。
|
|
42
|
+
|
|
43
|
+
**`@goodandready/dsh-cost-meter`** 在 DeepSeek Harness 会话顶部工具栏(`conversation.session.header.utilities`)无缝嵌入高精度花费微件芯片:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
● ≈ ¥0.85 0:17 ← 会话顶部实时花费芯片与换价倒计时
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
点击芯片可展开交互式详情面板:按每 1M Token 展示峰谷两档费率表格(高亮显示当前激活档位)、UTC 优惠时间段状态以及各模型在当前会话中的累计消费明细。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🏛️ 架构设计
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD
|
|
57
|
+
subgraph StreamTelemetry ["DSH 流式遥测运行时"]
|
|
58
|
+
Req["LLM 请求 / 报头<br/>(provider, model)"]
|
|
59
|
+
StreamHook["增量流式分片 Hook"]
|
|
60
|
+
Proj["costByModel 会话投影<br/>(半小时 UTC 时隙)"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
subgraph PricingCatalog ["费率解析引擎"]
|
|
64
|
+
Manual["自定义费率配置<br/>(settings.yaml: prices)"]
|
|
65
|
+
DeepSeekTier["DeepSeek 官方费率<br/>(高峰 / 闲时 50% 折扣)"]
|
|
66
|
+
OpenRouterTier["OpenRouter 公开目录<br/>(每日自动缓存)"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph UI ["用户交互界面"]
|
|
70
|
+
Chip["顶部花费芯片<br/>(实时计费 + 换价倒计时)"]
|
|
71
|
+
Modal["消费明细抽屉面板<br/>(1M 单价表, UTC 时段, 模型清单)"]
|
|
72
|
+
Settings["设置卡片<br/>(币种符号, 汇率, 时区)"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Req --> Proj
|
|
76
|
+
StreamHook --> Proj
|
|
77
|
+
Proj --> Chip
|
|
78
|
+
PricingCatalog --> Proj
|
|
79
|
+
Manual --> PricingCatalog
|
|
80
|
+
DeepSeekTier --> PricingCatalog
|
|
81
|
+
OpenRouterTier --> PricingCatalog
|
|
82
|
+
Chip --> Modal
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ✨ 核心特性
|
|
88
|
+
|
|
89
|
+
1. **增量流式实时计算**:在模型打字生成过程中毫秒级更新 Prompt、Completion 和 Cache 费用,无轮询无卡顿。
|
|
90
|
+
2. **两级阶梯电价引擎**:支持 DeepSeek 官方高峰时段(UTC 01:00–04:00 与 06:00–10:00)及闲时 50% 优惠折扣自动匹配。
|
|
91
|
+
3. **UTC 半小时时隙不可变性**:历史消费严格锁定在消耗发生时刻的时隙费率,切换费率不溯及既往。
|
|
92
|
+
4. **服务商智能路由**:精准区分直连 DeepSeek 服务商与 OpenRouter 等代理网关。
|
|
93
|
+
5. **多币种与本地化**:支持自定义货币符号(`¥`, `$`, `€`, `₽`)、汇率换算与显示时区。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📦 快速安装
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dsh plugin --profile web add @goodandready/dsh-cost-meter
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
重启 DeepSeek Harness 实例并刷新浏览器页面。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ⚙️ 配置项说明 (`settings.yaml`)
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
dsh-cost-meter:
|
|
111
|
+
currency: "¥"
|
|
112
|
+
usdRate: 7.2
|
|
113
|
+
displayTimeZone: "Asia/Shanghai"
|
|
114
|
+
useOpenRouter: true
|
|
115
|
+
prices: {}
|
|
116
|
+
modelMap: {}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 配置参数列表
|
|
120
|
+
|
|
121
|
+
| 参数项 | 类型 | 默认值 | 说明 |
|
|
122
|
+
|:---|:---|:---|:---|
|
|
123
|
+
| `currency` | `string` | `"¥"` | 显示的货币符号或代码 |
|
|
124
|
+
| `usdRate` | `number` | `7.2` | 美元到目标货币的换算汇率 |
|
|
125
|
+
| `displayTimeZone` | `string` | `"Asia/Shanghai"` | 界面中优惠时间段显示的参考时区 |
|
|
126
|
+
| `useOpenRouter` | `boolean` | `true` | 是否加载 OpenRouter 价格表作为备用 |
|
|
127
|
+
| `prices` | `object` | `{}` | 自定义每 1M Token 的费率覆盖 |
|
|
128
|
+
| `modelMap` | `object` | `{}` | 模型别名映射表 |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🧪 自动化测试
|
|
133
|
+
|
|
134
|
+
运行单元测试套件:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm test
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 📄 许可证
|
|
143
|
+
|
|
144
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/docs/README.ru.md
CHANGED
|
@@ -17,11 +17,21 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
21
|
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
22
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -144,3 +154,9 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
144
154
|
- **Серверный кэш и ETag**: Поддержка заголовка `304 Not Modified` для маршрута `/state` и кэширование сопоставлений моделей.
|
|
145
155
|
- **Ручная синхронизация**: Кнопка «Синхронизировать сейчас» в настройках с защитой от флуда (`POST /dsh-cost-meter/refresh`).
|
|
146
156
|
- **Подсказки экономии и порог бюджета**: Отсчёт времени до льготного окна (скидка 50%), порог сессии (`budgetThreshold`) и экспорт сводки в буфер обмена.
|
|
157
|
+
|
|
158
|
+
### Визуальные улучшения и языковой стандарт (v0.8.2)
|
|
159
|
+
- **Нативный точечный индикатор**: В шапке используется аккуратная статусная точка (зелёная — скидка 50%, янтарная — пик, красная пульсирующая — превышен бюджет).
|
|
160
|
+
- **Строка экономии кэша**: Отображение сэкономленных средств за счёт кэша контекста (например: `Кэш сэкономил: ≈ $0.14 (-68%)`).
|
|
161
|
+
- **Компактный список моделей**: Аккордеон со сворачиванием списка при использовании 3+ моделей в сессии.
|
|
162
|
+
- **Стандарт локализации DSH**: Встроенные словари `en` и `zh`. Русский язык предоставляется через `dsh-russian-lang`.
|
package/docs/README.zh.md
CHANGED
|
@@ -17,11 +17,21 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
21
|
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
22
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -132,3 +142,9 @@ npm test
|
|
|
132
142
|
## 📄 许可证
|
|
133
143
|
|
|
134
144
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
145
|
+
|
|
146
|
+
### 界面优化与语言规范 (v0.8.2)
|
|
147
|
+
- **原生状态圆点指示器**:顶部芯片采用更精简的原生状态圆点(绿色代表低谷优惠,琥珀色代表高峰,红色脉冲代表超出预算上限,灰色代表平价/空闲)。
|
|
148
|
+
- **上下文缓存节省提示**:自动计算并高亮显示命中提示词缓存所节省的费用(例如:`上下文缓存节省:≈ $0.14 (-68%)`)。
|
|
149
|
+
- **多模型折叠列表**:当单次会话中使用 3 个或更多模型时,提供紧凑的可折叠模型明细展示。
|
|
150
|
+
- **DSH 规范双语支持**:插件内置完整的英文(`en`)与中文(`zh`)语言字典。
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -86,6 +86,12 @@
|
|
|
86
86
|
- Не допускать утечек локальных путей и инфраструктурных данных в коде или документации.
|
|
87
87
|
|
|
88
88
|
## History & Decisions
|
|
89
|
+
- 2026-09-13 (v0.8.2):
|
|
90
|
+
- Приведение к стандарту локализации DSH (dsh-plugin-authoring): английский (en) — исходный и fallback-язык, китайский (zh) — обязательная пользовательская локаль. Русский словарь вынесен из плагина; все строки для русского интерфейса переданы в goodandready/dsh-russian-lang.
|
|
91
|
+
- Нативный точечный dot-индикатор статуса чипа в шапке (.dcm-dot): зеленый (off-peak), янтарный (peak), красный пульсирующий (budget exceeded), серый (idle/unknown).
|
|
92
|
+
- Адаптивное скрытие таймера чипа на экранах < 680px при сохранении суммы расхода.
|
|
93
|
+
- Расчёт и отображение выгоды от контекстного кэша (Cache Savings): строка 'Cache saved: ≈ $X.XX (-Y%)' при наличии cacheReadTokens.
|
|
94
|
+
- Компактный список моделей с аккордеоном при 3+ задействованных моделях сессии.
|
|
89
95
|
- 2026-09-12 (v0.8.1):
|
|
90
96
|
- Оптимизация производительности расчёта тарифов: замена 1440-минутного цикла на алгоритм O(K) по точкам перехода и мемоизация экстремумов через WeakMap.
|
|
91
97
|
- Серверный кэш разрешения тарифов resolveTariffCache (Map) с автоматической очисткой при обновлении каталога.
|
package/docs/plans/findings.md
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
# Findings: Issue #
|
|
1
|
+
# Findings: Issue #27 (v0.8.2)
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
- В сутках конечное число окон $K$ (обычно 2–4). Смена тарифа может происходить только на границах интервалов: `window.start` и `window.end`.
|
|
7
|
-
- Замена на $O(K)$ даёт мгновенный расчёт без аллокации сотен объектов в секунду при активных сессиях.
|
|
3
|
+
## 1. Точечный индикатор статуса
|
|
4
|
+
- Сплошной яркий фон чипа в тулбаре DSH перетягивает на себя внимание от основного чата.
|
|
5
|
+
- Нативный паттерн DSH: нейтральный полупрозрачный фон с цветной точкой (dot indicator 6x6px) и моноширинным текстом `tabular-nums`.
|
|
8
6
|
|
|
9
|
-
## 2.
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
## 2. Экономия кэша (Cache Savings)
|
|
8
|
+
- При использовании кэша провайдера (DeepSeek, Claude, Gemini) стоимость `cacheReadTokens` значительно ниже `uncachedInputTokens` (у DeepSeek: $0.014 vs $0.14-$0.44 за 1M токенов).
|
|
9
|
+
- Разница между стоимостью токенов без кэша (`cacheReadTokens * inputRate`) и фактической стоимостью (`cacheReadTokens * cacheHitRate`) наглядно демонстрирует реальную экономию пользователя прямо в поповере.
|
|
12
10
|
|
|
13
|
-
## 3.
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
## 4. Алиасы вендоров
|
|
17
|
-
- Разные провайдеры в DSH могут передавать вендоров как `z-ai`, `zai`, `google`, `gemini`, `deepseek-ai`, `deepseek`. Единая нормализация исключит ситуации fallback на generic тариф.
|
|
11
|
+
## 3. Стандарт локализации
|
|
12
|
+
- По стандарту DSH (`dsh-plugin-authoring`, `dhs-plugin-release-workflow`), плагин обязан иметь `en` как канонический исходный и `zh` как обязательную локаль.
|
|
13
|
+
- Русский перевод не зашивается в плагин, а поставляется через отдельный плагин `dsh-russian-lang`. Для синхронизации перевода открывается issue в репозитории `goodandready/dsh-russian-lang`.
|
package/docs/plans/progress.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# Progress: Issue #
|
|
1
|
+
# Progress: Issue #27 (v0.8.2)
|
|
2
2
|
|
|
3
|
-
- **2026-09-
|
|
4
|
-
- Создана Gitea Issue #
|
|
5
|
-
-
|
|
6
|
-
-
|
|
3
|
+
- **2026-09-13 15:06**:
|
|
4
|
+
- Создана Gitea Issue #27.
|
|
5
|
+
- Развернут worktree `.worktrees/issue-27` на ветке `feat/issue-27-ui-and-locales`.
|
|
6
|
+
- Зафиксированы планы и стандарты в `docs/plans/`.
|
|
7
7
|
|
|
8
|
-
- **2026-09-
|
|
9
|
-
- Реализованы
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
8
|
+
- **2026-09-13 15:08**:
|
|
9
|
+
- Реализованы улучшения в `lib/client.js` (dot status indicator, cache savings display, multi-model accordion, en/zh locales).
|
|
10
|
+
- Добавлены тесты в `test/v082-ui-locales.test.mjs` (все 22 теста успешно пройдены).
|
|
11
|
+
- Версия обновлена до `0.8.2`.
|
|
12
|
+
- Документация актуализирована на en, zh, ru.
|
package/docs/plans/task_plan.md
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
# Task Plan: Issue #
|
|
1
|
+
# Task Plan: Issue #27 (v0.8.2) — Визуальная индикация шапки/поповера и стандарт локализации en/zh
|
|
2
2
|
|
|
3
3
|
## Цели
|
|
4
|
-
1.
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Запрос подтверждения
|
|
4
|
+
1. **Визуальные улучшения шапки и поповера**:
|
|
5
|
+
- Чип в шапке: точечный dot-индикатор (зеленый/янтарный/красный/серый) вместо сплошного фона, аккуратная интеграция с тулбаром DSH.
|
|
6
|
+
- Адаптивное скрытие таймера на узких экранах, сохранение суммы.
|
|
7
|
+
- Индикация экономии контекстного кэша (Cache Savings): строка `Cache saved: ≈ $X.XX (-Y%)`.
|
|
8
|
+
- Компактный список/аккордеон при нескольких моделях сессии (3+ модели).
|
|
9
|
+
- Точные нативные токены тем DSH.
|
|
10
|
+
2. **Стандарт локализации DSH**:
|
|
11
|
+
- Исходный и fallback язык: `en`.
|
|
12
|
+
- Обязательная локаль: `zh` (полный набор эквивалентных ключей).
|
|
13
|
+
- Исключение зашитого `ru` словаря из плагина.
|
|
14
|
+
- Создание Issue в `goodandready/dsh-russian-lang` со всеми строками для русского перевода.
|
|
15
|
+
3. **Документация и релиз**:
|
|
16
|
+
- Версия `0.8.2` (инкремент только `z`).
|
|
17
|
+
- Актуализация `docs/design/DESIGN.md`, `README.md` (en), `docs/README.zh.md` (zh), `docs/README.ru.md` (ru).
|
|
18
|
+
- 100% покрытие unit-тестами без внешних зависимостей.
|
|
19
|
+
- Валидация на test server (`:3082`) и на production (`:3080`).
|
|
20
|
+
- Запрос подтверждения -> публикация v0.8.2.
|
|
21
21
|
|
|
22
22
|
## Статус выполнения
|
|
23
|
-
- [x] Issue #
|
|
24
|
-
- [x] Worktree `.worktrees/issue-
|
|
25
|
-
- [x] Реализация
|
|
26
|
-
- [x]
|
|
27
|
-
- [x]
|
|
28
|
-
- [
|
|
29
|
-
- [ ]
|
|
30
|
-
- [ ] Публикация релиза v0.8.
|
|
31
|
-
- [ ] Закрытие Issue #
|
|
23
|
+
- [x] Issue #27 создана
|
|
24
|
+
- [x] Worktree `.worktrees/issue-27` развернут
|
|
25
|
+
- [x] Реализация `lib/client.js` (UI шапки/поповера, расчет cache savings, словари en/zh)
|
|
26
|
+
- [x] Обновление тестов в `test/`
|
|
27
|
+
- [x] Обновление документации (`DESIGN.md`, `README.md`, `README.zh.md`, `README.ru.md`)
|
|
28
|
+
- [ ] Регистрация Issue в `goodandready/dsh-russian-lang`
|
|
29
|
+
- [ ] Сборка и приёмка кандидата (:3082, :3080)
|
|
30
|
+
- [ ] Публикация релиза v0.8.2
|
|
31
|
+
- [ ] Закрытие Issue #27 и cleanup worktree
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// dsh-cost-meter — client half.
|
|
2
2
|
// Self-registering browser module for DeepSeek Harness.
|
|
3
|
-
// Unified design system aligned with @goodandready/dsh-clinebot.
|
|
3
|
+
// Unified design system aligned with @goodandready/dsh-clinebot and DSH tokens.
|
|
4
|
+
// Canonical languages: English (en) and Chinese (zh).
|
|
4
5
|
|
|
5
6
|
window.__ModuleLoader__.load({
|
|
6
7
|
id: '@goodandready/dsh-cost-meter',
|
|
@@ -16,62 +17,6 @@ window.__ModuleLoader__.load({
|
|
|
16
17
|
|
|
17
18
|
// ----------------------------------------------------------- localization
|
|
18
19
|
|
|
19
|
-
const ru = {
|
|
20
|
-
title: 'Учёт расходов',
|
|
21
|
-
subtitle: 'Тариф моделей, окна пиковых цен и стоимость сессии',
|
|
22
|
-
'header.title': 'Учёт расходов (Cost Meter)',
|
|
23
|
-
'header.sub': 'Мониторинг расходов токенов, автоматическое сопоставление тарифов и скидочных окон',
|
|
24
|
-
'badge.catalog_ok': 'Каталог: {count} моделей',
|
|
25
|
-
'badge.catalog_empty': 'Каталог: загрузка…',
|
|
26
|
-
'badge.catalog_off': 'Каталог: выключен',
|
|
27
|
-
'badge.catalog_err': 'Каталог: ошибка',
|
|
28
|
-
'badge.catalog_stale': 'Кэш: офлайн',
|
|
29
|
-
'badge.deepseek': 'Тариф DeepSeek: активен',
|
|
30
|
-
'section.general': 'Основные параметры',
|
|
31
|
-
'section.general_desc': 'Отображение валюты, курса пересчёта, лимита бюджета и часового пояса',
|
|
32
|
-
'section.openrouter': 'Каталог цен OpenRouter',
|
|
33
|
-
'section.openrouter_desc': 'Публичный каталог моделей для автоматического определения ставок и окон скидок',
|
|
34
|
-
'section.diagnostics': 'Диагностика и статус',
|
|
35
|
-
'section.diagnostics_desc': 'Текущее состояние фоновой синхронизации и кэша тарифов',
|
|
36
|
-
'diag.models_count': 'Моделей в каталоге',
|
|
37
|
-
'diag.last_sync': 'Последняя синхронизация',
|
|
38
|
-
'diag.route': 'Активный маршрут',
|
|
39
|
-
'diag.tariff_source': 'Источник тарифа',
|
|
40
|
-
'field.currency': 'Символ валюты',
|
|
41
|
-
'hint.currency': 'Символ валюты для отображения расходов (например, $, ₽, €).',
|
|
42
|
-
'field.usdRate': 'Курс валюты к USD',
|
|
43
|
-
'hint.usdRate': 'Количество единиц вашей валюты за 1 USD (1 для долларов).',
|
|
44
|
-
'field.budgetThreshold': 'Порог бюджета сессии (USD)',
|
|
45
|
-
'hint.budgetThreshold': 'Оповещать цветом чипа при превышении суммы за сессию (0 — выключено).',
|
|
46
|
-
'field.displayTimeZone': 'Временная зона (IANA)',
|
|
47
|
-
'hint.displayTimeZone': 'Временная зона для отображения пиковых окон (например, Europe/Moscow, UTC).',
|
|
48
|
-
'field.useOpenRouter': 'Использовать каталог OpenRouter',
|
|
49
|
-
'hint.useOpenRouter': 'Автоматически загружать ставки и скидочные окна для моделей через OpenRouter API.',
|
|
50
|
-
'field.refreshHours': 'Период обновления каталога (часов)',
|
|
51
|
-
'hint.refreshHours': 'Интервал фонового обновления каталога в часах (от 1 до 168).',
|
|
52
|
-
'action.sync_now': 'Синхронизировать сейчас',
|
|
53
|
-
'action.syncing': 'Синхронизация…',
|
|
54
|
-
'action.sync_ok': 'Каталог обновлён',
|
|
55
|
-
'action.sync_err': 'Ошибка синхронизации',
|
|
56
|
-
'action.sync_rate_limited': 'Подождите {seconds} сек',
|
|
57
|
-
'action.copy_report': 'Скопировать сводку',
|
|
58
|
-
'action.copied': 'Скопировано!',
|
|
59
|
-
'savings.hint': '💡 Скидка 50% начнётся через {time} ({clock} {zone})',
|
|
60
|
-
'budget.exceeded': '⚠️ Превышен лимит сессии: {limit}',
|
|
61
|
-
'settings.save': 'Сохранить настройки',
|
|
62
|
-
'settings.saving': 'Сохранение…',
|
|
63
|
-
'settings.saved': 'Настройки успешно сохранены',
|
|
64
|
-
'settings.saveFailed': 'Ошибка сохранения: ',
|
|
65
|
-
'settings.loading': 'Загрузка настроек…',
|
|
66
|
-
'settings.unavailable': 'Настройки временно недоступны',
|
|
67
|
-
'settings.retry': 'Повторить попытку',
|
|
68
|
-
chipUnknown: 'Тариф не определён',
|
|
69
|
-
routeUnknown: 'Маршрут не определён',
|
|
70
|
-
catalogUnavailable: 'Каталог недоступен: ',
|
|
71
|
-
peak: 'пик',
|
|
72
|
-
offpeak: 'не-пик',
|
|
73
|
-
}
|
|
74
|
-
|
|
75
20
|
const en = {
|
|
76
21
|
title: 'Cost Meter',
|
|
77
22
|
subtitle: 'Model rates, peak discount windows and session spend',
|
|
@@ -94,13 +39,13 @@ window.__ModuleLoader__.load({
|
|
|
94
39
|
'diag.route': 'Active route',
|
|
95
40
|
'diag.tariff_source': 'Tariff source',
|
|
96
41
|
'field.currency': 'Currency Symbol',
|
|
97
|
-
'hint.currency': 'Currency symbol shown in the UI (e.g. $,
|
|
42
|
+
'hint.currency': 'Currency symbol shown in the UI (e.g. $, ¥, €).',
|
|
98
43
|
'field.usdRate': 'USD Exchange Rate',
|
|
99
44
|
'hint.usdRate': 'Units of currency per 1 USD (1 keeps USD).',
|
|
100
45
|
'field.budgetThreshold': 'Session Budget Threshold (USD)',
|
|
101
46
|
'hint.budgetThreshold': 'Warn via chip color when session spend exceeds this amount (0 disables).',
|
|
102
47
|
'field.displayTimeZone': 'Time Zone (IANA)',
|
|
103
|
-
'hint.displayTimeZone': 'IANA time zone for peak hours display (e.g. Europe/Moscow, UTC).',
|
|
48
|
+
'hint.displayTimeZone': 'IANA time zone for peak hours display (e.g. Europe/Moscow, Asia/Shanghai, UTC).',
|
|
104
49
|
'field.useOpenRouter': 'Use OpenRouter Catalog',
|
|
105
50
|
'hint.useOpenRouter': 'Pull rates and discount windows dynamically from public OpenRouter API.',
|
|
106
51
|
'field.refreshHours': 'Catalog Refresh Interval (hours)',
|
|
@@ -113,7 +58,10 @@ window.__ModuleLoader__.load({
|
|
|
113
58
|
'action.copy_report': 'Copy Summary',
|
|
114
59
|
'action.copied': 'Copied!',
|
|
115
60
|
'savings.hint': '💡 50% discount starts in {time} ({clock} {zone})',
|
|
61
|
+
'savings.cache_saved': '⚡ Context cache saved: ≈ {amount} ({percent}%)',
|
|
116
62
|
'budget.exceeded': '⚠️ Budget limit exceeded: {limit}',
|
|
63
|
+
'models.show_more': 'Show all {count} models',
|
|
64
|
+
'models.show_less': 'Show fewer',
|
|
117
65
|
'settings.save': 'Save Settings',
|
|
118
66
|
'settings.saving': 'Saving…',
|
|
119
67
|
'settings.saved': 'Settings saved successfully',
|
|
@@ -126,6 +74,89 @@ window.__ModuleLoader__.load({
|
|
|
126
74
|
catalogUnavailable: 'Catalog unavailable: ',
|
|
127
75
|
peak: 'peak',
|
|
128
76
|
offpeak: 'off-peak',
|
|
77
|
+
'rate.per_1m': '1M tokens, $',
|
|
78
|
+
'rate.cache_hit': 'Input (cache hit)',
|
|
79
|
+
'rate.input': 'Input (cache miss)',
|
|
80
|
+
'rate.cache_write': 'Input (cache write)',
|
|
81
|
+
'rate.output': 'Output',
|
|
82
|
+
'tokens.session': 'Session Tokens',
|
|
83
|
+
'tokens.tokens_col': 'Tokens',
|
|
84
|
+
'tokens.models_spent': 'Spend by Model',
|
|
85
|
+
'tokens.session_total': 'Session total:',
|
|
86
|
+
'tokens.no_usage_yet': 'Provider has not reported usage for this session yet.',
|
|
87
|
+
'tokens.not_priced': 'Not priced',
|
|
88
|
+
'tz.invalid': 'Invalid IANA time zone identifier',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const zh = {
|
|
92
|
+
title: '成本计量器',
|
|
93
|
+
subtitle: '模型费率、高峰优惠窗口与会话消费统计',
|
|
94
|
+
'header.title': '成本计量器 (Cost Meter)',
|
|
95
|
+
'header.sub': 'Token 消费监控、费率自动匹配与折扣窗口管理',
|
|
96
|
+
'badge.catalog_ok': '目录:{count} 个模型',
|
|
97
|
+
'badge.catalog_empty': '目录:加载中…',
|
|
98
|
+
'badge.catalog_off': '目录:已禁用',
|
|
99
|
+
'badge.catalog_err': '目录:错误',
|
|
100
|
+
'badge.catalog_stale': '缓存:离线',
|
|
101
|
+
'badge.deepseek': 'DeepSeek 费率:生效中',
|
|
102
|
+
'section.general': '基础参数',
|
|
103
|
+
'section.general_desc': '显示货币、汇率转换、预算阈值与时区设置',
|
|
104
|
+
'section.openrouter': 'OpenRouter 定价目录',
|
|
105
|
+
'section.openrouter_desc': '用于自动获取模型价格与折扣窗口的公开价格目录',
|
|
106
|
+
'section.diagnostics': '诊断与状态',
|
|
107
|
+
'section.diagnostics_desc': '后台同步与费率缓存当前状态',
|
|
108
|
+
'diag.models_count': '目录中模型数',
|
|
109
|
+
'diag.last_sync': '上次同步',
|
|
110
|
+
'diag.route': '当前路由',
|
|
111
|
+
'diag.tariff_source': '费率来源',
|
|
112
|
+
'field.currency': '货币符号',
|
|
113
|
+
'hint.currency': '界面中显示的货币符号(例如:$、¥、€)。',
|
|
114
|
+
'field.usdRate': 'USD 汇率',
|
|
115
|
+
'hint.usdRate': '每 1 美元对应的本币金额(1 为美元)。',
|
|
116
|
+
'field.budgetThreshold': '会话预算阈值 (USD)',
|
|
117
|
+
'hint.budgetThreshold': '当会话消费超出该金额时在状态指示器报警(0 表示禁用)。',
|
|
118
|
+
'field.displayTimeZone': '时区 (IANA)',
|
|
119
|
+
'hint.displayTimeZone': '显示高峰时段的 IANA 时区(例如:Asia/Shanghai、UTC)。',
|
|
120
|
+
'field.useOpenRouter': '使用 OpenRouter 目录',
|
|
121
|
+
'hint.useOpenRouter': '通过公开的 OpenRouter API 自动拉取费率与折扣窗口。',
|
|
122
|
+
'field.refreshHours': '目录刷新周期 (小时)',
|
|
123
|
+
'hint.refreshHours': '后台刷新价格目录的时间间隔(1 至 168 小时)。',
|
|
124
|
+
'action.sync_now': '立即同步',
|
|
125
|
+
'action.syncing': '同步中…',
|
|
126
|
+
'action.sync_ok': '目录已更新',
|
|
127
|
+
'action.sync_err': '同步失败',
|
|
128
|
+
'action.sync_rate_limited': '请等待 {seconds} 秒',
|
|
129
|
+
'action.copy_report': '复制消费报告',
|
|
130
|
+
'action.copied': '已复制!',
|
|
131
|
+
'savings.hint': '💡 5 折优惠将在 {time} 后生效 ({clock} {zone})',
|
|
132
|
+
'savings.cache_saved': '⚡ 上下文缓存节省:≈ {amount} ({percent}%)',
|
|
133
|
+
'budget.exceeded': '⚠️ 已超出预算上限:{limit}',
|
|
134
|
+
'models.show_more': '查看全部 {count} 个模型',
|
|
135
|
+
'models.show_less': '收起',
|
|
136
|
+
'settings.save': '保存设置',
|
|
137
|
+
'settings.saving': '保存中…',
|
|
138
|
+
'settings.saved': '设置已成功保存',
|
|
139
|
+
'settings.saveFailed': '保存设置失败:',
|
|
140
|
+
'settings.loading': '加载设置中…',
|
|
141
|
+
'settings.unavailable': '设置暂时不可用',
|
|
142
|
+
'settings.retry': '重试',
|
|
143
|
+
chipUnknown: '费率未知',
|
|
144
|
+
routeUnknown: '路由未知',
|
|
145
|
+
catalogUnavailable: '价格目录不可用:',
|
|
146
|
+
peak: '高峰',
|
|
147
|
+
offpeak: '低谷',
|
|
148
|
+
'rate.per_1m': '每 1M tokens ($)',
|
|
149
|
+
'rate.cache_hit': '输入 (命中缓存)',
|
|
150
|
+
'rate.input': '输入 (未命中)',
|
|
151
|
+
'rate.cache_write': '输入 (写入缓存)',
|
|
152
|
+
'rate.output': '输出',
|
|
153
|
+
'tokens.session': '会话 Tokens',
|
|
154
|
+
'tokens.tokens_col': 'Tokens 数',
|
|
155
|
+
'tokens.models_spent': '各模型消费',
|
|
156
|
+
'tokens.session_total': '会话总计:',
|
|
157
|
+
'tokens.no_usage_yet': '提供商尚未报告当前会话的用量数据。',
|
|
158
|
+
'tokens.not_priced': '未定价',
|
|
159
|
+
'tz.invalid': '无效的 IANA 时区标识',
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
function makeT(dict, fallback) {
|
|
@@ -144,8 +175,12 @@ window.__ModuleLoader__.load({
|
|
|
144
175
|
const localeService = (ctx && typeof ctx.get === 'function' ? ctx.get('locale') : null) || (ctx && ctx.locale) || (ctx && ctx.services && ctx.services.locale)
|
|
145
176
|
return React.useSyncExternalStore(
|
|
146
177
|
React.useMemo(() => (cb) => (localeService && typeof localeService.subscribe === 'function' ? localeService.subscribe(cb) : () => {}), [localeService]),
|
|
147
|
-
React.useCallback(() =>
|
|
148
|
-
|
|
178
|
+
React.useCallback(() => {
|
|
179
|
+
const snap = localeService && typeof localeService.getSnapshot === 'function' ? localeService.getSnapshot() : null
|
|
180
|
+
const active = snap ? (snap.active || snap.locale || 'en') : 'en'
|
|
181
|
+
return String(active).startsWith('zh') ? 'zh' : 'en'
|
|
182
|
+
}, [localeService]),
|
|
183
|
+
React.useCallback(() => 'en', []),
|
|
149
184
|
)
|
|
150
185
|
}
|
|
151
186
|
|
|
@@ -190,6 +225,7 @@ window.__ModuleLoader__.load({
|
|
|
190
225
|
.dcm-alert-ok { background: rgba(16,185,129,0.1); color: var(--dsw-alias-state-success-primary); }
|
|
191
226
|
.dcm-alert-err { background: rgba(239,68,68,0.1); color: var(--dsw-alias-state-error-primary); }
|
|
192
227
|
.dcm-tip-box { padding: 8px 12px; border-radius: 8px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.4; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
228
|
+
.dcm-cache-box { padding: 7px 11px; border-radius: 8px; background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.22); color: var(--dsw-alias-state-success-primary); font-size: 11.5px; font-weight: 500; line-height: 1.4; font-variant-numeric: tabular-nums; }
|
|
193
229
|
|
|
194
230
|
.dcm-stat-box { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-3); display: flex; flex-direction: column; gap: 2px; }
|
|
195
231
|
.dcm-stat-val { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
@@ -197,26 +233,39 @@ window.__ModuleLoader__.load({
|
|
|
197
233
|
|
|
198
234
|
.dcm-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding-top: 6px; }
|
|
199
235
|
|
|
200
|
-
|
|
201
|
-
.dcm-chip
|
|
202
|
-
.dcm-chip
|
|
203
|
-
|
|
236
|
+
/* Native dot-indicator chip */
|
|
237
|
+
.dcm-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.05)); color: var(--dsw-alias-label-primary); cursor: pointer; font: inherit; font-size: 12px; font-variant-numeric: tabular-nums; line-height: 24px; transition: all .15s ease; user-select: none; }
|
|
238
|
+
.dcm-chip:hover { border-color: var(--dsw-alias-border-l3, var(--dsw-alias-label-tertiary)); background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.08)); }
|
|
239
|
+
.dcm-dot { width: 6.5px; height: 6.5px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
240
|
+
.dcm-dot-ok { background: var(--dsw-alias-state-success-primary, #10b981); box-shadow: 0 0 5px rgba(16,185,129,0.5); }
|
|
241
|
+
.dcm-dot-warn { background: var(--dsw-alias-state-warning-primary, #f59e0b); box-shadow: 0 0 5px rgba(245,158,11,0.4); }
|
|
242
|
+
.dcm-dot-err { background: var(--dsw-alias-state-error-primary, #ef4444); box-shadow: 0 0 6px rgba(239,68,68,0.7); animation: dcm-pulse 1.8s infinite ease-in-out; }
|
|
243
|
+
.dcm-dot-idle { background: var(--dsw-alias-label-tertiary, #9ca3af); }
|
|
244
|
+
|
|
245
|
+
.dcm-chip-timer { opacity: 0.72; font-size: 11px; padding-left: 2px; border-left: 1px solid var(--dsw-alias-border-l2); }
|
|
246
|
+
@media (max-width: 680px) {
|
|
247
|
+
.dcm-chip-timer { display: none; }
|
|
248
|
+
}
|
|
204
249
|
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
@keyframes dcm-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } }
|
|
251
|
+
|
|
252
|
+
.dcm-panel { position: absolute; top: 30px; right: 0; z-index: 40; width: 336px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base)); box-shadow: 0 10px 32px rgba(0,0,0,.32); font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-primary); text-align: left; font-variant-numeric: tabular-nums; }
|
|
253
|
+
.dcm-panel-title { font-weight: 600; padding-bottom: 2px; font-size: 13.5px; display: flex; justify-content: space-between; align-items: baseline; }
|
|
207
254
|
.dcm-panel-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
208
255
|
.dcm-hr { height: 1px; background: var(--dsw-alias-border-l2); margin: 10px -16px; border: 0; }
|
|
209
|
-
.dcm-panel-grid { display: grid; grid-template-columns: 1fr
|
|
210
|
-
.dcm-panel-grid-flat { display: grid; grid-template-columns: 1fr
|
|
211
|
-
.dcm-panel-grid-3 { display: grid; grid-template-columns: 1fr
|
|
256
|
+
.dcm-panel-grid { display: grid; grid-template-columns: 1fr 62px 62px; column-gap: 6px; align-items: center; }
|
|
257
|
+
.dcm-panel-grid-flat { display: grid; grid-template-columns: 1fr 76px; column-gap: 6px; align-items: center; }
|
|
258
|
+
.dcm-panel-grid-3 { display: grid; grid-template-columns: 1fr 60px 74px; column-gap: 6px; align-items: center; }
|
|
212
259
|
.dcm-th { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; }
|
|
213
260
|
.dcm-th-num { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; text-align: right; font-size: 11px; }
|
|
214
|
-
.dcm-td { padding:
|
|
261
|
+
.dcm-td { padding: 3.5px 0; color: var(--dsw-alias-label-secondary); }
|
|
215
262
|
.dcm-cell-idle { text-align: right; padding: 3px 6px; border-radius: 4px; color: var(--dsw-alias-label-tertiary); }
|
|
216
|
-
.dcm-num { text-align: right; color: var(--dsw-alias-label-tertiary); padding:
|
|
217
|
-
.dcm-cost { text-align: right; padding:
|
|
263
|
+
.dcm-num { text-align: right; color: var(--dsw-alias-label-tertiary); padding: 3.5px 0; }
|
|
264
|
+
.dcm-cost { text-align: right; padding: 3.5px 0; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
218
265
|
.dcm-section-lbl { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
219
|
-
.dcm-total { display: flex; justify-content: space-between; font-weight: 600; font-size:
|
|
266
|
+
.dcm-total { display: flex; justify-content: space-between; font-weight: 600; font-size: 13.5px; }
|
|
267
|
+
.dcm-accordion-btn { appearance: none; background: transparent; border: 0; color: var(--dsw-alias-state-brand-primary, #6366f1); font: inherit; font-size: 11.5px; cursor: pointer; padding: 2px 0; text-align: left; }
|
|
268
|
+
.dcm-accordion-btn:hover { text-decoration: underline; }
|
|
220
269
|
`
|
|
221
270
|
|
|
222
271
|
function ensureStyles() {
|
|
@@ -324,13 +373,14 @@ window.__ModuleLoader__.load({
|
|
|
324
373
|
}
|
|
325
374
|
}
|
|
326
375
|
|
|
327
|
-
function ago(timestamp) {
|
|
328
|
-
if (!timestamp) return '
|
|
376
|
+
function ago(timestamp, locale) {
|
|
377
|
+
if (!timestamp) return '—'
|
|
378
|
+
const isZh = locale === 'zh'
|
|
329
379
|
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60000))
|
|
330
|
-
if (minutes < 1) return '
|
|
331
|
-
if (minutes < 60) return minutes + '
|
|
380
|
+
if (minutes < 1) return isZh ? '刚刚' : 'just now'
|
|
381
|
+
if (minutes < 60) return minutes + (isZh ? ' 分钟前' : ' min ago')
|
|
332
382
|
const hours = Math.round(minutes / 60)
|
|
333
|
-
return hours < 48 ? hours + '
|
|
383
|
+
return hours < 48 ? hours + (isZh ? ' 小时前' : ' hrs ago') : Math.round(hours / 24) + (isZh ? ' 天前' : ' days ago')
|
|
334
384
|
}
|
|
335
385
|
|
|
336
386
|
// ---------------------------------------------------------------- card
|
|
@@ -338,7 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
338
388
|
function CostMeterCard(props) {
|
|
339
389
|
const ctx = props.ctx
|
|
340
390
|
const locale = useActiveLocale(ctx)
|
|
341
|
-
const t = props.t || makeT(locale === '
|
|
391
|
+
const t = props.t || makeT(locale === 'zh' ? zh : en, en)
|
|
342
392
|
const [open, setOpen] = React.useState(false)
|
|
343
393
|
const [draft, setDraft] = React.useState(null)
|
|
344
394
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -563,7 +613,7 @@ window.__ModuleLoader__.load({
|
|
|
563
613
|
onChange: (e) => setDraft({ ...draft, displayTimeZone: e.target.value }),
|
|
564
614
|
}),
|
|
565
615
|
React.createElement('span', { className: 'dcm-hint' },
|
|
566
|
-
tzValid ? t('hint.displayTimeZone') : '
|
|
616
|
+
tzValid ? t('hint.displayTimeZone') : t('tz.invalid')
|
|
567
617
|
),
|
|
568
618
|
),
|
|
569
619
|
),
|
|
@@ -622,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
622
672
|
),
|
|
623
673
|
React.createElement('div', { className: 'dcm-stat-box' },
|
|
624
674
|
React.createElement('span', { className: 'dcm-stat-val' },
|
|
625
|
-
diagState ? ago(diagState.fetchedAt) : '—'
|
|
675
|
+
diagState ? ago(diagState.fetchedAt, locale) : '—'
|
|
626
676
|
),
|
|
627
677
|
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.last_sync')),
|
|
628
678
|
),
|
|
@@ -669,14 +719,10 @@ window.__ModuleLoader__.load({
|
|
|
669
719
|
return (schedule && schedule.base) || { cacheHit: 0, input: 0, cacheWrite: 0, output: 0 }
|
|
670
720
|
}
|
|
671
721
|
|
|
672
|
-
/**
|
|
673
|
-
* Fast O(K) lookup of next tariff rate boundary instead of a 1440-minute loop.
|
|
674
|
-
*/
|
|
675
722
|
function minutesUntilChange(schedule, minute) {
|
|
676
723
|
const windows = (schedule && schedule.windows) || []
|
|
677
724
|
if (!windows.length) return null
|
|
678
725
|
const now = ratesAt(schedule, minute)
|
|
679
|
-
// Transition points can only occur at window start or window end
|
|
680
726
|
const transitions = new Set()
|
|
681
727
|
for (const w of windows) {
|
|
682
728
|
transitions.add(w.start % DAY)
|
|
@@ -694,9 +740,6 @@ window.__ModuleLoader__.load({
|
|
|
694
740
|
return minDiff
|
|
695
741
|
}
|
|
696
742
|
|
|
697
|
-
/**
|
|
698
|
-
* Memoized calculation of low and high rate extremes for a schedule.
|
|
699
|
-
*/
|
|
700
743
|
const extremesMemo = new WeakMap()
|
|
701
744
|
function extremes(schedule) {
|
|
702
745
|
if (!schedule) return { low: {}, high: {}, flat: true }
|
|
@@ -780,7 +823,7 @@ window.__ModuleLoader__.load({
|
|
|
780
823
|
reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), 0, minute,
|
|
781
824
|
))
|
|
782
825
|
try {
|
|
783
|
-
return new Intl.DateTimeFormat('
|
|
826
|
+
return new Intl.DateTimeFormat('en-GB', {
|
|
784
827
|
hour: '2-digit', minute: '2-digit', hour12: false, timeZone: zone,
|
|
785
828
|
}).format(d)
|
|
786
829
|
} catch {
|
|
@@ -790,7 +833,6 @@ window.__ModuleLoader__.load({
|
|
|
790
833
|
|
|
791
834
|
function zoneLabel(zone) {
|
|
792
835
|
if (!zone) return 'UTC'
|
|
793
|
-
if (zone === 'Europe/Moscow') return 'МСК'
|
|
794
836
|
const city = zone.includes('/') ? zone.slice(zone.lastIndexOf('/') + 1) : zone
|
|
795
837
|
return city.replace(/_/g, ' ')
|
|
796
838
|
}
|
|
@@ -800,7 +842,7 @@ window.__ModuleLoader__.load({
|
|
|
800
842
|
function CostMeter(props) {
|
|
801
843
|
const ctx = props.ctx
|
|
802
844
|
const locale = useActiveLocale(ctx)
|
|
803
|
-
const t = props.t || makeT(locale === '
|
|
845
|
+
const t = props.t || makeT(locale === 'zh' ? zh : en, en)
|
|
804
846
|
const useProjection = props.useProjection
|
|
805
847
|
const usage = useProjection ? useProjection('tokenUsage') : undefined
|
|
806
848
|
const byModel = useProjection ? useProjection('costByModel') : undefined
|
|
@@ -808,6 +850,7 @@ window.__ModuleLoader__.load({
|
|
|
808
850
|
const [open, setOpen] = React.useState(false)
|
|
809
851
|
const [now, setNow] = React.useState(() => new Date())
|
|
810
852
|
const [copied, setCopied] = React.useState(false)
|
|
853
|
+
const [showAllModels, setShowAllModels] = React.useState(false)
|
|
811
854
|
const box = React.useRef(null)
|
|
812
855
|
|
|
813
856
|
React.useEffect(() => { ensureStyles() }, [])
|
|
@@ -868,16 +911,18 @@ window.__ModuleLoader__.load({
|
|
|
868
911
|
h('button', {
|
|
869
912
|
key: 'chip', type: 'button',
|
|
870
913
|
className: 'dcm-chip',
|
|
871
|
-
style: { background: 'transparent', color: 'var(--dsw-alias-label-tertiary)' },
|
|
872
914
|
onClick: () => setOpen((v) => !v), title: t('chipUnknown'),
|
|
873
|
-
},
|
|
915
|
+
}, [
|
|
916
|
+
h('span', { key: 'dot', className: 'dcm-dot dcm-dot-idle' }),
|
|
917
|
+
h('span', { key: 'txt' }, t('chipUnknown')),
|
|
918
|
+
]),
|
|
874
919
|
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
875
920
|
h('div', { key: 't', className: 'dcm-panel-title' },
|
|
876
921
|
state.route ? state.route.provider + ' / ' + state.route.model : t('routeUnknown')),
|
|
877
922
|
h('div', { key: 'n', className: 'dcm-panel-note' },
|
|
878
923
|
state.catalogError
|
|
879
924
|
? t('catalogUnavailable') + state.catalogError
|
|
880
|
-
: '
|
|
925
|
+
: t('tokens.not_priced')),
|
|
881
926
|
]) : null,
|
|
882
927
|
])
|
|
883
928
|
}
|
|
@@ -910,13 +955,12 @@ window.__ModuleLoader__.load({
|
|
|
910
955
|
const totalUsd = fx > 0 ? total / fx : total
|
|
911
956
|
const budgetExceeded = budgetThreshold > 0 && totalUsd >= budgetThreshold
|
|
912
957
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: bg, color: fg, fontWeight: 600 }
|
|
958
|
+
// Dot indicator status class
|
|
959
|
+
const dotClass = budgetExceeded
|
|
960
|
+
? 'dcm-dot dcm-dot-err'
|
|
961
|
+
: (flat ? 'dcm-dot dcm-dot-idle' : (cheapNow ? 'dcm-dot dcm-dot-ok' : 'dcm-dot dcm-dot-warn'))
|
|
962
|
+
|
|
963
|
+
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: 'rgba(99,102,241,0.12)', color: 'var(--dsw-alias-state-brand-primary, #6366f1)', fontWeight: 600 }
|
|
920
964
|
|
|
921
965
|
const rateRow = (label, key) => flat
|
|
922
966
|
? [
|
|
@@ -956,16 +1000,30 @@ window.__ModuleLoader__.load({
|
|
|
956
1000
|
.join(', ')
|
|
957
1001
|
const nowLocal = hhmmInZone(minute, zone, now)
|
|
958
1002
|
|
|
959
|
-
//
|
|
1003
|
+
// Calculate cache savings amount
|
|
1004
|
+
let cacheSavedAmount = 0
|
|
1005
|
+
let cacheSavedPercent = 0
|
|
1006
|
+
if (hasUsage && usage && Number(usage.cacheReadTokens) > 0 && active.input > active.cacheHit) {
|
|
1007
|
+
const tokensRead = Number(usage.cacheReadTokens)
|
|
1008
|
+
const missedCost = (tokensRead / 1e6) * active.input
|
|
1009
|
+
const hitCost = (tokensRead / 1e6) * active.cacheHit
|
|
1010
|
+
cacheSavedAmount = Math.max(0, (missedCost - hitCost) * fx)
|
|
1011
|
+
cacheSavedPercent = missedCost > 0 ? Math.round(((missedCost - hitCost) / missedCost) * 100) : 0
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Copy summary report to clipboard
|
|
960
1015
|
const copyReport = () => {
|
|
961
1016
|
const lines = [
|
|
962
1017
|
'=== ' + (state.route ? state.route.model : 'Cost Meter Report') + ' ===',
|
|
963
|
-
'
|
|
964
|
-
'
|
|
965
|
-
'
|
|
1018
|
+
t('tokens.session_total') + ' ' + (hasUsage ? money(total, currency) : '—') + (budgetThreshold > 0 ? ' (limit: ' + money(budgetThreshold * fx, currency) + ')' : ''),
|
|
1019
|
+
'Tier: ' + (flat ? 'Flat' : (cheapNow ? t('offpeak') : t('peak'))),
|
|
1020
|
+
'Rates (1M): input ' + rate(active.input) + ', output ' + rate(active.output) + ', cache ' + rate(active.cacheHit),
|
|
966
1021
|
]
|
|
1022
|
+
if (cacheSavedAmount > 0) {
|
|
1023
|
+
lines.push('Cache saved: ≈ ' + money(cacheSavedAmount, currency) + ' (' + cacheSavedPercent + '%)')
|
|
1024
|
+
}
|
|
967
1025
|
if (hasUsage && usage) {
|
|
968
|
-
lines.push('
|
|
1026
|
+
lines.push('Tokens: in ' + tokens(usage.inputTokens) + ', out ' + tokens(usage.outputTokens) + ', cache hit ' + tokens(usage.cacheReadTokens))
|
|
969
1027
|
}
|
|
970
1028
|
const text = lines.join('\n')
|
|
971
1029
|
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
@@ -991,57 +1049,65 @@ window.__ModuleLoader__.load({
|
|
|
991
1049
|
})
|
|
992
1050
|
}
|
|
993
1051
|
|
|
1052
|
+
// Display models with collapse support if > 3 models
|
|
1053
|
+
const visibleModels = (perModel.length
|
|
1054
|
+
? perModel
|
|
1055
|
+
: (state.route ? [{ key: state.route.provider + '/' + state.route.model, model: state.route.model, known: Boolean(schedule), cost: 0 }] : [])
|
|
1056
|
+
)
|
|
1057
|
+
const shouldCollapse = visibleModels.length > 3 && !showAllModels
|
|
1058
|
+
const renderedModels = shouldCollapse ? visibleModels.slice(0, 3) : visibleModels
|
|
1059
|
+
|
|
994
1060
|
return h('div', { ref: box, style: { position: 'relative' } }, [
|
|
995
1061
|
h('button', {
|
|
996
1062
|
key: 'chip', type: 'button',
|
|
997
|
-
className: 'dcm-chip'
|
|
998
|
-
style: {
|
|
999
|
-
background: bg,
|
|
1000
|
-
color: fg,
|
|
1001
|
-
borderColor: budgetExceeded
|
|
1002
|
-
? 'rgba(239,68,68,0.5)'
|
|
1003
|
-
: (cheapNow ? 'rgba(16,185,129,0.3)' : 'rgba(245,158,11,0.3)'),
|
|
1004
|
-
},
|
|
1063
|
+
className: 'dcm-chip',
|
|
1005
1064
|
onClick: () => setOpen((v) => !v),
|
|
1006
1065
|
title: budgetExceeded
|
|
1007
1066
|
? t('budget.exceeded', { limit: money(budgetThreshold * fx, currency) })
|
|
1008
|
-
: (flat ? '
|
|
1067
|
+
: (flat ? 'Flat tariff' : cheapNow ? t('offpeak') : t('peak')),
|
|
1009
1068
|
}, [
|
|
1069
|
+
h('span', { key: 'dot', className: dotClass }),
|
|
1010
1070
|
h('span', { key: 'sum' }, hasUsage ? '≈ ' + money(total, currency) : '—'),
|
|
1011
|
-
left ? h('span', { key: 'left',
|
|
1071
|
+
left ? h('span', { key: 'left', className: 'dcm-chip-timer' }, left) : null,
|
|
1012
1072
|
]),
|
|
1013
1073
|
|
|
1014
1074
|
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
1015
|
-
h('div', { key: 'title', className: 'dcm-panel-title' },
|
|
1016
|
-
(state.route ? state.route.model : '
|
|
1017
|
-
|
|
1075
|
+
h('div', { key: 'title', className: 'dcm-panel-title' }, [
|
|
1076
|
+
h('span', { key: 'model-name' }, state.route ? state.route.model : 'model'),
|
|
1077
|
+
flat ? null : h('span', { key: 'tier', style: { fontSize: 11.5, opacity: 0.85 } }, cheapNow ? t('offpeak') : t('peak')),
|
|
1078
|
+
]),
|
|
1018
1079
|
|
|
1019
1080
|
h('div', { key: 'note', className: 'dcm-panel-note' },
|
|
1020
1081
|
flat
|
|
1021
|
-
? '
|
|
1022
|
-
: (cheapNow ? '
|
|
1023
|
-
(left ? ' ·
|
|
1082
|
+
? 'Flat rate schedule'
|
|
1083
|
+
: (cheapNow ? 'Discounted rate active' : 'Full rate active') +
|
|
1084
|
+
(left ? ' · change in ' + left : '')),
|
|
1024
1085
|
|
|
1025
1086
|
flat
|
|
1026
1087
|
? null
|
|
1027
1088
|
: h('div', { key: 'clock', className: 'dcm-panel-note' },
|
|
1028
|
-
'
|
|
1089
|
+
t('peak') + ': ' + windowText + ' ' + zl + ' · now ' + nowLocal + ' ' + zl),
|
|
1029
1090
|
|
|
1030
1091
|
discountHint ? h('div', { key: 'savings-box', className: 'dcm-tip-box', style: { marginTop: 6 } }, discountHint) : null,
|
|
1031
1092
|
|
|
1093
|
+
cacheSavedAmount > 0
|
|
1094
|
+
? h('div', { key: 'cache-saved-box', className: 'dcm-cache-box', style: { marginTop: 6 } },
|
|
1095
|
+
t('savings.cache_saved', { amount: money(cacheSavedAmount, currency), percent: cacheSavedPercent }))
|
|
1096
|
+
: null,
|
|
1097
|
+
|
|
1032
1098
|
budgetExceeded ? h('div', { key: 'budget-warn', className: 'dcm-alert dcm-alert-err', style: { marginTop: 6, padding: '6px 10px', fontSize: 11 } },
|
|
1033
1099
|
t('budget.exceeded', { limit: money(budgetThreshold * fx, currency) })) : null,
|
|
1034
1100
|
|
|
1035
1101
|
h('hr', { key: 'hr1', className: 'dcm-hr' }),
|
|
1036
1102
|
|
|
1037
1103
|
h('div', { key: 'rates', className: flat ? 'dcm-panel-grid-flat' : 'dcm-panel-grid' }, [
|
|
1038
|
-
h('div', { key: 'h0', className: 'dcm-th' }, '
|
|
1039
|
-
flat ? null : h('div', { key: 'h1', className: 'dcm-th-num' }, '
|
|
1040
|
-
h('div', { key: 'h2', className: 'dcm-th-num' }, flat ? '
|
|
1041
|
-
rateRow('
|
|
1042
|
-
rateRow('
|
|
1043
|
-
showCacheWrite ? rateRow('
|
|
1044
|
-
rateRow('
|
|
1104
|
+
h('div', { key: 'h0', className: 'dcm-th' }, t('rate.per_1m')),
|
|
1105
|
+
flat ? null : h('div', { key: 'h1', className: 'dcm-th-num' }, t('offpeak')),
|
|
1106
|
+
h('div', { key: 'h2', className: 'dcm-th-num' }, flat ? 'rate' : t('peak')),
|
|
1107
|
+
rateRow(t('rate.cache_hit'), 'cacheHit'),
|
|
1108
|
+
rateRow(t('rate.input'), 'input'),
|
|
1109
|
+
showCacheWrite ? rateRow(t('rate.cache_write'), 'cacheWrite') : null,
|
|
1110
|
+
rateRow(t('rate.output'), 'output'),
|
|
1045
1111
|
]),
|
|
1046
1112
|
|
|
1047
1113
|
h('hr', { key: 'hr2', className: 'dcm-hr' }),
|
|
@@ -1049,27 +1115,24 @@ window.__ModuleLoader__.load({
|
|
|
1049
1115
|
multiModel
|
|
1050
1116
|
? null
|
|
1051
1117
|
: [
|
|
1052
|
-
h('div', { key: 'btitle', className: 'dcm-section-lbl' }, '
|
|
1118
|
+
h('div', { key: 'btitle', className: 'dcm-section-lbl' }, t('tokens.session')),
|
|
1053
1119
|
h('div', { key: 'breakdown', className: 'dcm-panel-grid-3' }, [
|
|
1054
|
-
h('div', { key: 'b0', className: 'dcm-th' }, '
|
|
1120
|
+
h('div', { key: 'b0', className: 'dcm-th' }, t('tokens.tokens_col')),
|
|
1055
1121
|
h('div', { key: 'b1', className: 'dcm-th-num' }, ''),
|
|
1056
1122
|
h('div', { key: 'b2', className: 'dcm-th-num' }, currency),
|
|
1057
|
-
spentRow('
|
|
1058
|
-
spentRow('
|
|
1059
|
-
showCacheWrite ? spentRow('
|
|
1060
|
-
spentRow('
|
|
1123
|
+
spentRow(t('rate.cache_hit'), 'cacheReadTokens', 'cacheHit'),
|
|
1124
|
+
spentRow(t('rate.input'), 'uncachedInputTokens', 'input'),
|
|
1125
|
+
showCacheWrite ? spentRow(t('rate.cache_write'), 'cacheWriteTokens', 'cacheWrite') : null,
|
|
1126
|
+
spentRow(t('rate.output'), 'outputTokens', 'output'),
|
|
1061
1127
|
]),
|
|
1062
1128
|
h('hr', { key: 'hr-models', className: 'dcm-hr' }),
|
|
1063
1129
|
],
|
|
1064
1130
|
|
|
1065
|
-
h('div', { key: 'mtitle', className: 'dcm-section-lbl' }, '
|
|
1131
|
+
h('div', { key: 'mtitle', className: 'dcm-section-lbl' }, t('tokens.models_spent')),
|
|
1066
1132
|
h(
|
|
1067
1133
|
'div',
|
|
1068
1134
|
{ key: 'models-list', style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
1069
|
-
(
|
|
1070
|
-
? perModel
|
|
1071
|
-
: (state.route ? [{ key: state.route.provider + '/' + state.route.model, model: state.route.model, known: Boolean(schedule), cost: 0 }] : [])
|
|
1072
|
-
).map((m) =>
|
|
1135
|
+
renderedModels.map((m) =>
|
|
1073
1136
|
h(
|
|
1074
1137
|
'div',
|
|
1075
1138
|
{
|
|
@@ -1102,22 +1165,31 @@ window.__ModuleLoader__.load({
|
|
|
1102
1165
|
color: m.known ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
1103
1166
|
},
|
|
1104
1167
|
},
|
|
1105
|
-
m.known ? (hasUsage ? money(m.cost, currency) : '—') : '
|
|
1168
|
+
m.known ? (hasUsage ? money(m.cost, currency) : '—') : t('tokens.not_priced'),
|
|
1106
1169
|
),
|
|
1107
1170
|
],
|
|
1108
1171
|
),
|
|
1109
1172
|
),
|
|
1110
1173
|
),
|
|
1111
1174
|
|
|
1175
|
+
visibleModels.length > 3
|
|
1176
|
+
? h('button', {
|
|
1177
|
+
key: 'toggle-models',
|
|
1178
|
+
type: 'button',
|
|
1179
|
+
className: 'dcm-accordion-btn',
|
|
1180
|
+
onClick: () => setShowAllModels((v) => !v),
|
|
1181
|
+
}, shouldCollapse ? t('models.show_more', { count: visibleModels.length }) : t('models.show_less'))
|
|
1182
|
+
: null,
|
|
1183
|
+
|
|
1112
1184
|
h('hr', { key: 'hr3', className: 'dcm-hr' }),
|
|
1113
1185
|
|
|
1114
1186
|
h('div', { key: 'total', className: 'dcm-total' }, [
|
|
1115
|
-
h('span', { key: 'l' }, '
|
|
1116
|
-
h('span', { key: 'v' }, hasUsage ? '≈ ' + money(total, currency) : '
|
|
1187
|
+
h('span', { key: 'l' }, t('tokens.session_total')),
|
|
1188
|
+
h('span', { key: 'v' }, hasUsage ? '≈ ' + money(total, currency) : '—'),
|
|
1117
1189
|
]),
|
|
1118
1190
|
|
|
1119
1191
|
hasUsage ? null : h('div', { key: 'nodata', className: 'dcm-panel-note', style: { paddingTop: 6 } },
|
|
1120
|
-
'
|
|
1192
|
+
t('tokens.no_usage_yet')),
|
|
1121
1193
|
|
|
1122
1194
|
h('div', { key: 'footer-actions', style: { display: 'flex', justifyContent: 'flex-end', marginTop: 10 } },
|
|
1123
1195
|
h('button', {
|
|
@@ -1132,7 +1204,7 @@ window.__ModuleLoader__.load({
|
|
|
1132
1204
|
|
|
1133
1205
|
function apply(ctx) {
|
|
1134
1206
|
if (ctx.locale && ctx.locale.register) {
|
|
1135
|
-
try { ctx.locale.register(NS, { en,
|
|
1207
|
+
try { ctx.locale.register(NS, { en, zh }) } catch (_) {}
|
|
1136
1208
|
}
|
|
1137
1209
|
|
|
1138
1210
|
refreshMirrorUntilVisible(ctx)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cost-meter",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|