@goodandready/dsh-cost-meter 0.8.2 → 0.8.4

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/lib/updater.js ADDED
@@ -0,0 +1,270 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const UPDATE_HEADER = 'x-dsh-plugin-update'
9
+ const UPDATE_TIMEOUT_MS = 10 * 60_000
10
+ const VERSION_CACHE_MS = 5 * 60_000
11
+ let latestCache = undefined
12
+
13
+ function header(request, name) {
14
+ const value = request?.headers?.[name]
15
+ return Array.isArray(value) ? value[0] : value
16
+ }
17
+
18
+ function isLoopback(value) {
19
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
20
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
21
+ || address?.startsWith('127.') === true
22
+ || address?.startsWith('::ffff:127.') === true
23
+ }
24
+
25
+ export function isTrustedUpdateRequest(request) {
26
+ if (header(request, UPDATE_HEADER) !== '1') return false
27
+ if (!isLoopback(request?.socket?.remoteAddress)) return false
28
+ const site = header(request, 'sec-fetch-site')
29
+ if (site !== undefined && site !== 'same-origin') return false
30
+ const origin = header(request, 'origin')
31
+ const host = header(request, 'host')
32
+ if (origin === undefined || host === undefined) return false
33
+ try {
34
+ const url = new URL(origin)
35
+ return (url.protocol === 'http:' || url.protocol === 'https:')
36
+ && isLoopback(url.hostname) && url.host === host
37
+ } catch (e) {
38
+ return false
39
+ }
40
+ }
41
+
42
+ function validProfileName(value) {
43
+ return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
44
+ && !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value)
45
+ }
46
+
47
+ function profileNameFromArgv(argv) {
48
+ if (!Array.isArray(argv)) return undefined
49
+ for (let index = 2; index < argv.length; index += 1) {
50
+ if (argv[index] === '--profile') return argv[index + 1]
51
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
52
+ }
53
+ return argv[2] === 'web' ? 'web' : undefined
54
+ }
55
+
56
+ function findDshCliEntry() {
57
+ const value = process.argv[1]
58
+ if (typeof value !== 'string' || value.length === 0) return undefined
59
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
60
+ if (!existsSync(entry)) return undefined
61
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
62
+ const manifestPath = resolve(directory, 'package.json')
63
+ if (existsSync(manifestPath)) {
64
+ try {
65
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
66
+ const bin = typeof manifest.bin === 'string'
67
+ ? manifest.bin
68
+ : typeof manifest.bin === 'object' && manifest.bin !== null
69
+ ? manifest.bin.dsh
70
+ : undefined
71
+ if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
72
+ && !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry
73
+ } catch (e) {
74
+ /* continue */
75
+ }
76
+ }
77
+ const parent = dirname(directory)
78
+ if (parent === directory) return undefined
79
+ }
80
+ }
81
+
82
+ function detectRuntime() {
83
+ const profileDir = resolve(process.env.DSH_PROFILE_DIR
84
+ ?? resolve(homedir(), '.dsh', 'profiles', 'web'))
85
+ const selected = profileNameFromArgv(process.argv)
86
+ const profileName = validProfileName(selected)
87
+ ? selected
88
+ : validProfileName(basename(profileDir)) ? basename(profileDir) : 'web'
89
+ const cliEntry = findDshCliEntry()
90
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
91
+ }
92
+
93
+ function parseSemver(value) {
94
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value || ''))
95
+ if (match === null) return undefined
96
+ return {
97
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
98
+ prerelease: match[4]?.split('.') ?? [],
99
+ }
100
+ }
101
+
102
+ function comparePrerelease(candidateParts, currentParts) {
103
+ if (candidateParts.length === 0 && currentParts.length > 0) return 1
104
+ if (candidateParts.length > 0 && currentParts.length === 0) return -1
105
+ if (candidateParts.length === 0 && currentParts.length === 0) return 0
106
+
107
+ const len = Math.max(candidateParts.length, currentParts.length)
108
+ for (let i = 0; i < len; i += 1) {
109
+ const a = candidateParts[i]
110
+ const b = currentParts[i]
111
+ if (a === undefined) return -1
112
+ if (b === undefined) return 1
113
+ if (a === b) continue
114
+
115
+ const aNum = /^\d+$/.test(a) ? Number(a) : undefined
116
+ const bNum = /^\d+$/.test(b) ? Number(b) : undefined
117
+
118
+ if (aNum !== undefined && bNum !== undefined) {
119
+ return aNum > bNum ? 1 : -1
120
+ }
121
+ if (aNum !== undefined && bNum === undefined) {
122
+ return -1
123
+ }
124
+ if (aNum === undefined && bNum !== undefined) {
125
+ return 1
126
+ }
127
+ return a.localeCompare(b) > 0 ? 1 : -1
128
+ }
129
+ return 0
130
+ }
131
+
132
+ export function isNewerVersion(currentValue, candidateValue) {
133
+ const current = parseSemver(currentValue)
134
+ const candidate = parseSemver(candidateValue)
135
+ if (current === undefined || candidate === undefined) return false
136
+ for (let index = 0; index < 3; index += 1) {
137
+ if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index]
138
+ }
139
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0
140
+ }
141
+
142
+ async function fetchLatestVersion(packageName, registry) {
143
+ if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
144
+ return latestCache.version
145
+ }
146
+ try {
147
+ const reg = registry.replace(/\/+$/, '')
148
+ const response = await fetch(`${reg}/${encodeURIComponent(packageName)}/latest`, {
149
+ signal: AbortSignal.timeout(6000),
150
+ })
151
+ if (!response.ok) return undefined
152
+ const value = await response.json()
153
+ if (typeof value?.version !== 'string' || !value.version) return undefined
154
+ latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS }
155
+ return value.version
156
+ } catch (e) {
157
+ return undefined
158
+ }
159
+ }
160
+
161
+ async function readCurrentVersion(manifestUrl) {
162
+ const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
163
+ if (typeof value?.version !== 'string' || !value.version) throw new Error('Cannot read current plugin version.')
164
+ return value.version
165
+ }
166
+
167
+ export async function checkUpdateStatus(options, target = detectRuntime()) {
168
+ const current = await readCurrentVersion(options.manifestUrl)
169
+ const latest = await fetchLatestVersion(options.packageName, options.registry || 'https://registry.npmjs.org')
170
+ return {
171
+ packageName: options.packageName,
172
+ currentVersion: current,
173
+ ...(latest === undefined ? {} : { latestVersion: latest }),
174
+ latestCheckFailed: latest === undefined,
175
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
176
+ profileName: target.profileName,
177
+ canAutoUpdate: target.cliEntry !== undefined,
178
+ }
179
+ }
180
+
181
+ async function installExact(target, packageSpec, options) {
182
+ if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
183
+ await new Promise((resolvePromise, reject) => {
184
+ const child = spawn(process.execPath, [
185
+ target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
186
+ packageSpec,
187
+ `--registry=${options.registry || 'https://registry.npmjs.org/'}`,
188
+ ], {
189
+ cwd: target.profileDir,
190
+ windowsHide: true,
191
+ stdio: ['ignore', 'pipe', 'pipe'],
192
+ env: { ...process.env, NO_COLOR: '1' },
193
+ })
194
+ let detail = ''
195
+ child.stdout?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
196
+ child.stderr?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
197
+ const timer = setTimeout(() => {
198
+ child.kill()
199
+ reject(new Error('Update timed out.'))
200
+ }, UPDATE_TIMEOUT_MS)
201
+ child.once('error', (error) => { clearTimeout(timer); reject(error) })
202
+ child.once('exit', (code) => {
203
+ clearTimeout(timer)
204
+ if (code === 0) resolvePromise()
205
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
206
+ })
207
+ })
208
+ }
209
+
210
+ function writeResponseJson(response, statusCode, value) {
211
+ response.writeHead(statusCode, {
212
+ 'content-type': 'application/json; charset=utf-8',
213
+ 'cache-control': 'no-store',
214
+ })
215
+ response.end(JSON.stringify(value))
216
+ }
217
+
218
+ export function registerPluginUpdater(ctx, options) {
219
+ if (!ctx?.webServer?.register) return () => {}
220
+ let installing = false
221
+ return ctx.webServer.register({
222
+ kind: 'exact',
223
+ path: options.endpoint,
224
+ handler: async (request, response) => {
225
+ try {
226
+ const target = detectRuntime()
227
+ if (request.method === 'GET' || request.method === 'HEAD') {
228
+ const payload = await checkUpdateStatus(options, target)
229
+ writeResponseJson(response, 200, payload)
230
+ return
231
+ }
232
+ if (request.method !== 'POST') {
233
+ response.writeHead(405, { allow: 'GET, HEAD, POST' })
234
+ response.end()
235
+ return
236
+ }
237
+ if (!isTrustedUpdateRequest(request)) {
238
+ writeResponseJson(response, 403, { ok: false, error: 'Rejected non-local or cross-origin update request.' })
239
+ return
240
+ }
241
+ if (installing) {
242
+ writeResponseJson(response, 409, { ok: false, error: 'This plugin is already updating.' })
243
+ return
244
+ }
245
+ installing = true
246
+ try {
247
+ const before = await checkUpdateStatus(options, target)
248
+ if (before.latestVersion === undefined) {
249
+ writeResponseJson(response, 503, { ok: false, error: 'The latest version is temporarily unavailable.' })
250
+ return
251
+ }
252
+ if (!before.updateAvailable) {
253
+ writeResponseJson(response, 200, before)
254
+ return
255
+ }
256
+ await installExact(target, `${options.packageName}@${before.latestVersion}`, options)
257
+ writeResponseJson(response, 200, {
258
+ ...before,
259
+ updatedVersion: before.latestVersion,
260
+ restartRequired: true,
261
+ })
262
+ } finally {
263
+ installing = false
264
+ }
265
+ } catch (error) {
266
+ writeResponseJson(response, 500, { ok: false, error: String(error?.message || error) })
267
+ }
268
+ },
269
+ })
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cost-meter",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -11,10 +11,11 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "files": [
14
- "lib",
14
+ "lib/",
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
- "docs",
17
+ "README.ru.md",
18
+ "README.zh.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "publishConfig": {
@@ -26,7 +27,9 @@
26
27
  },
27
28
  "client": {
28
29
  "platform": "web",
29
- "inject": []
30
+ "inject": [
31
+ "@deepseek-ai/dsh-client-locale"
32
+ ]
30
33
  }
31
34
  },
32
35
  "keywords": [
package/docs/README.ru.md DELETED
@@ -1,162 +0,0 @@
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`) и экспорт сводки в буфер обмена.
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 DELETED
@@ -1,150 +0,0 @@
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)
145
-
146
- ### 界面优化与语言规范 (v0.8.2)
147
- - **原生状态圆点指示器**:顶部芯片采用更精简的原生状态圆点(绿色代表低谷优惠,琥珀色代表高峰,红色脉冲代表超出预算上限,灰色代表平价/空闲)。
148
- - **上下文缓存节省提示**:自动计算并高亮显示命中提示词缓存所节省的费用(例如:`上下文缓存节省:≈ $0.14 (-68%)`)。
149
- - **多模型折叠列表**:当单次会话中使用 3 个或更多模型时,提供紧凑的可折叠模型明细展示。
150
- - **DSH 规范双语支持**:插件内置完整的英文(`en`)与中文(`zh`)语言字典。
@@ -1,9 +0,0 @@
1
- # Публичный релиз и deployment
2
-
3
- Релизная версия повышается patch-изменением (v0.7.6). После merge в `main`
4
- создаются annotated tag, пакет в npm (`@goodandready/dsh-cost-meter`) и
5
- публичный GitHub release в `GooDAnDReaDY/dsh-cost-meter`.
6
-
7
- Production должен содержать exact package version из npm и не должен иметь
8
- локальной зависимости на исходный checkout. После restart проверяются
9
- service health, browser loader и `GET /dsh-cost-meter/state`.