dsh-usage-badge 0.1.0 → 0.1.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 +3 -1
- package/lib/client.js +105 -41
- package/lib/official-pricing.js +16 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,7 +32,9 @@ dsh plugin --profile web remove dsh-usage-badge
|
|
|
32
32
|
|
|
33
33
|
弹窗有两个页签:
|
|
34
34
|
|
|
35
|
-
- **用量** ——
|
|
35
|
+
- **用量** —— 四个时间区间(`24 小时` / `近 7 日` / `近 30 日` / `近 12 个月`)、按 provider 筛选、金额/请求数/Token/缓存命中率四项合计,以及一张四序列折线图(金额、token 总量、请求数、缓存命中率)。鼠标移到图上显示该点的四项数值和参考线。
|
|
36
|
+
|
|
37
|
+
区间按**自然日 / 自然月**切分:`近 7 日` 就是最近 7 个连续自然日,没有用量的那天保留自己的空槽位,不会把窗口拉长成 8 天。
|
|
36
38
|
- **单价配置** —— 官方定价、法定节假日日历、当前生效单价。
|
|
37
39
|
|
|
38
40
|
## 单价:只有一个来源
|
package/lib/client.js
CHANGED
|
@@ -173,6 +173,46 @@ window.__ModuleLoader__.load({
|
|
|
173
173
|
return out
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
const pad2 = (value) => String(value).padStart(2, '0')
|
|
177
|
+
|
|
178
|
+
/** The local `YYYY-MM-DD` key of a Date. */
|
|
179
|
+
const dayKeyOf = (date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The last `count` calendar days ending at `endKey`, oldest first.
|
|
183
|
+
*
|
|
184
|
+
* Enumerated from the calendar, not from the days that happen to have usage: the
|
|
185
|
+
* host only reports days with requests, so taking "the last 7 of those" stretches
|
|
186
|
+
* the window to whatever 7 days last had data and hides the gaps instead of
|
|
187
|
+
* showing them. A quiet Saturday is still a day.
|
|
188
|
+
*/
|
|
189
|
+
function lastDays(endKey, count) {
|
|
190
|
+
const [year, month, day] = endKey.split('-').map(Number)
|
|
191
|
+
const cursor = new Date(year, month - 1, day)
|
|
192
|
+
const out = []
|
|
193
|
+
for (let index = 0; index < count; index++) {
|
|
194
|
+
out.unshift(dayKeyOf(cursor))
|
|
195
|
+
cursor.setDate(cursor.getDate() - 1)
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The last `count` calendar months ending at `endKey`'s month, oldest first. */
|
|
201
|
+
function lastMonths(endKey, count) {
|
|
202
|
+
const [year, month] = endKey.split('-').map(Number)
|
|
203
|
+
const out = []
|
|
204
|
+
for (let index = count - 1; index >= 0; index--) {
|
|
205
|
+
const cursor = new Date(year, month - 1 - index, 1)
|
|
206
|
+
out.push(`${cursor.getFullYear()}-${pad2(cursor.getMonth() + 1)}`)
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The day a snapshot is anchored on: the host's own "today". */
|
|
212
|
+
function anchorDate(snapshot) {
|
|
213
|
+
return snapshot.badge?.date ?? snapshot.today?.date ?? snapshot.days?.[0]?.date ?? ''
|
|
214
|
+
}
|
|
215
|
+
|
|
176
216
|
/**
|
|
177
217
|
* Build the chart points and totals for one range under one provider filter.
|
|
178
218
|
*
|
|
@@ -207,30 +247,35 @@ window.__ModuleLoader__.load({
|
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
if (range === '12m') {
|
|
210
|
-
const
|
|
250
|
+
const byMonth = new Map()
|
|
211
251
|
for (const day of snapshot.days) {
|
|
212
252
|
const key = day.date.slice(0, 7)
|
|
213
|
-
if (!
|
|
214
|
-
|
|
253
|
+
if (!byMonth.has(key)) byMonth.set(key, [])
|
|
254
|
+
byMonth.get(key).push(entryTotals(day, provider))
|
|
215
255
|
}
|
|
216
|
-
|
|
256
|
+
// Twelve calendar months, so a month with no usage keeps its empty slot
|
|
257
|
+
// instead of quietly pushing an older month into the window.
|
|
217
258
|
return {
|
|
218
|
-
points:
|
|
259
|
+
points: lastMonths(anchorDate(snapshot), 12).map((key) =>
|
|
260
|
+
point(key.slice(2), key, sumTotals(byMonth.get(key) ?? [])),
|
|
261
|
+
),
|
|
219
262
|
marks: false,
|
|
220
263
|
}
|
|
221
264
|
}
|
|
222
265
|
|
|
223
266
|
const count = range === '7d' ? 7 : 30
|
|
224
|
-
const
|
|
267
|
+
const byDate = new Map(snapshot.days.map((day) => [day.date, day]))
|
|
225
268
|
return {
|
|
226
|
-
points:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
points: lastDays(anchorDate(snapshot), count).map((date) => {
|
|
270
|
+
const day = byDate.get(date)
|
|
271
|
+
if (!day) return point(date.slice(5), `${date}(无用量)`, EMPTY_TOTALS)
|
|
272
|
+
return point(
|
|
273
|
+
date.slice(5),
|
|
274
|
+
`${date}${day.dayName ? ` ${day.dayName}` : ''}`,
|
|
230
275
|
entryTotals(day, provider),
|
|
231
276
|
day.dayClass,
|
|
232
|
-
)
|
|
233
|
-
),
|
|
277
|
+
)
|
|
278
|
+
}),
|
|
234
279
|
marks: true,
|
|
235
280
|
}
|
|
236
281
|
}
|
|
@@ -469,8 +514,8 @@ window.__ModuleLoader__.load({
|
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
/** The 用量 panel: ranges, provider filter, totals and the chart. */
|
|
472
|
-
function UsagePanel({ snapshot }) {
|
|
473
|
-
const [range, setRange] = React.useState(
|
|
517
|
+
function UsagePanel({ snapshot, initialRange = '24h' }) {
|
|
518
|
+
const [range, setRange] = React.useState(initialRange)
|
|
474
519
|
const [provider, setProvider] = React.useState('')
|
|
475
520
|
const providers = snapshot.today.providers || []
|
|
476
521
|
const series = buildSeries(snapshot, range, provider)
|
|
@@ -544,6 +589,17 @@ window.__ModuleLoader__.load({
|
|
|
544
589
|
)
|
|
545
590
|
}
|
|
546
591
|
|
|
592
|
+
/**
|
|
593
|
+
* The last successful 单价配置 payloads, kept for the life of the page.
|
|
594
|
+
*
|
|
595
|
+
* Switching tabs unmounts this panel, so without a cache every return to the tab
|
|
596
|
+
* starts from the loading placeholder and swaps its contents in a moment later —
|
|
597
|
+
* a visible flicker. Holding the last answer lets the panel render it straight
|
|
598
|
+
* away and refresh behind it.
|
|
599
|
+
*/
|
|
600
|
+
let cachedConfig = null
|
|
601
|
+
let cachedOfficial = null
|
|
602
|
+
|
|
547
603
|
/**
|
|
548
604
|
* The 单价配置 panel.
|
|
549
605
|
*
|
|
@@ -554,32 +610,38 @@ window.__ModuleLoader__.load({
|
|
|
554
610
|
*/
|
|
555
611
|
function ConfigPanel({ snapshot, reload, initialConfig = null, initialOfficial = null }) {
|
|
556
612
|
const [source, setSource] = React.useState('zh-cn')
|
|
557
|
-
const [official, setOfficial] = React.useState(initialOfficial)
|
|
558
|
-
const [officialState, setOfficialState] = React.useState(
|
|
559
|
-
|
|
560
|
-
const [config, setConfig] = React.useState(initialConfig)
|
|
561
|
-
|
|
562
|
-
const loadOfficial = React.useCallback(
|
|
563
|
-
async (which) => {
|
|
564
|
-
setOfficialState('loading')
|
|
565
|
-
try {
|
|
566
|
-
const response = await fetch(`/usage-badge/official-pricing?refresh=1&source=${which}`, { cache: 'no-store' })
|
|
567
|
-
const body = await response.json()
|
|
568
|
-
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`)
|
|
569
|
-
setOfficial(body)
|
|
570
|
-
setOfficialState('ready')
|
|
571
|
-
} catch (error) {
|
|
572
|
-
setOfficial(null)
|
|
573
|
-
setOfficialState(`failed: ${error?.message ?? error}`)
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
[],
|
|
613
|
+
const [official, setOfficial] = React.useState(initialOfficial ?? cachedOfficial)
|
|
614
|
+
const [officialState, setOfficialState] = React.useState(
|
|
615
|
+
(initialOfficial ?? cachedOfficial) ? 'ready' : 'loading',
|
|
577
616
|
)
|
|
617
|
+
const [applyState, setApplyState] = React.useState('idle')
|
|
618
|
+
const [config, setConfig] = React.useState(initialConfig ?? cachedConfig)
|
|
619
|
+
|
|
620
|
+
const loadOfficial = React.useCallback(async (which) => {
|
|
621
|
+
// Only show the placeholder when there is nothing to show: flipping to
|
|
622
|
+
// 'loading' with a table already on screen blanks it for a frame.
|
|
623
|
+
if (!cachedOfficial) setOfficialState('loading')
|
|
624
|
+
try {
|
|
625
|
+
const response = await fetch(`/usage-badge/official-pricing?refresh=1&source=${which}`, { cache: 'no-store' })
|
|
626
|
+
const body = await response.json()
|
|
627
|
+
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`)
|
|
628
|
+
cachedOfficial = body
|
|
629
|
+
setOfficial(body)
|
|
630
|
+
setOfficialState('ready')
|
|
631
|
+
} catch (error) {
|
|
632
|
+
// A failed refresh keeps the last good list on screen; the warning says why.
|
|
633
|
+
if (!cachedOfficial) setOfficial(null)
|
|
634
|
+
setOfficialState(`failed: ${error?.message ?? error}`)
|
|
635
|
+
}
|
|
636
|
+
}, [])
|
|
578
637
|
|
|
579
638
|
const loadConfig = React.useCallback(async () => {
|
|
580
639
|
try {
|
|
581
640
|
const response = await fetch('/usage-badge/config', { cache: 'no-store' })
|
|
582
|
-
if (response.ok)
|
|
641
|
+
if (response.ok) {
|
|
642
|
+
cachedConfig = await response.json()
|
|
643
|
+
setConfig(cachedConfig)
|
|
644
|
+
}
|
|
583
645
|
} catch {
|
|
584
646
|
// The effective config is informative here; a failure leaves the rest of
|
|
585
647
|
// the panel usable.
|
|
@@ -850,7 +912,7 @@ window.__ModuleLoader__.load({
|
|
|
850
912
|
)
|
|
851
913
|
}
|
|
852
914
|
|
|
853
|
-
function UsageDialog({ snapshot, onClose, reload, initialTab = 'usage', initialConfig = null, initialOfficial = null }) {
|
|
915
|
+
function UsageDialog({ snapshot, onClose, reload, initialTab = 'usage', initialConfig = null, initialOfficial = null, initialRange = '24h' }) {
|
|
854
916
|
const [tab, setTab] = React.useState(initialTab)
|
|
855
917
|
const chip = bandChip(snapshot)
|
|
856
918
|
|
|
@@ -893,10 +955,10 @@ window.__ModuleLoader__.load({
|
|
|
893
955
|
),
|
|
894
956
|
),
|
|
895
957
|
),
|
|
896
|
-
// Only the active panel is mounted, so returning to 单价配置 re-fetches
|
|
897
|
-
//
|
|
958
|
+
// Only the active panel is mounted, so returning to 单价配置 re-fetches the
|
|
959
|
+
// published list — from the cache it renders at once, then refreshes.
|
|
898
960
|
tab === 'usage'
|
|
899
|
-
? h(UsagePanel, { snapshot })
|
|
961
|
+
? h(UsagePanel, { snapshot, initialRange })
|
|
900
962
|
: h(ConfigPanel, { snapshot, reload, initialConfig, initialOfficial }),
|
|
901
963
|
),
|
|
902
964
|
)
|
|
@@ -913,7 +975,8 @@ window.__ModuleLoader__.load({
|
|
|
913
975
|
* seat: the full row while expanded, a 36px square on the collapsed rail.
|
|
914
976
|
*
|
|
915
977
|
* @param {{wide?: boolean, initialSnapshot?: object|null, initialOpen?: boolean,
|
|
916
|
-
* initialTab?: string, initialConfig?: object|null, initialOfficial?: object|null
|
|
978
|
+
* initialTab?: string, initialConfig?: object|null, initialOfficial?: object|null,
|
|
979
|
+
* initialRange?: string}} props
|
|
917
980
|
*/
|
|
918
981
|
function UsageBadge({
|
|
919
982
|
wide = true,
|
|
@@ -922,6 +985,7 @@ window.__ModuleLoader__.load({
|
|
|
922
985
|
initialTab = 'usage',
|
|
923
986
|
initialConfig = null,
|
|
924
987
|
initialOfficial = null,
|
|
988
|
+
initialRange = '24h',
|
|
925
989
|
} = {}) {
|
|
926
990
|
const [snapshot, setSnapshot] = React.useState(initialSnapshot)
|
|
927
991
|
const [failed, setFailed] = React.useState(false)
|
|
@@ -972,7 +1036,7 @@ window.__ModuleLoader__.load({
|
|
|
972
1036
|
h('span', { className: 'dub-pill-amount' }, text),
|
|
973
1037
|
),
|
|
974
1038
|
open && snapshot
|
|
975
|
-
? h(UsageDialog, { snapshot, onClose: () => setOpen(false), reload: load, initialTab, initialConfig, initialOfficial })
|
|
1039
|
+
? h(UsageDialog, { snapshot, onClose: () => setOpen(false), reload: load, initialTab, initialConfig, initialOfficial, initialRange })
|
|
976
1040
|
: null,
|
|
977
1041
|
)
|
|
978
1042
|
}
|
package/lib/official-pricing.js
CHANGED
|
@@ -91,16 +91,28 @@ function parseAmount(text) {
|
|
|
91
91
|
function toLocalRanges(ranges, sourceOffsetMinutes, localOffsetMinutes) {
|
|
92
92
|
const delta = localOffsetMinutes - sourceOffsetMinutes
|
|
93
93
|
const round = (value) => Math.round(value * 100) / 100
|
|
94
|
+
const wrap = (minutes) => ((minutes % 1440) + 1440) % 1440
|
|
94
95
|
const out = []
|
|
95
96
|
for (const [start, end] of ranges) {
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
let startMinutes = start * 60 + delta
|
|
98
|
+
let endMinutes = end * 60 + delta
|
|
99
|
+
|
|
100
|
+
// A window pushed entirely out of the day wraps as a whole. Only a window that
|
|
101
|
+
// straddles midnight splits in two — splitting an out-of-range one instead
|
|
102
|
+
// produced nonsense ranges (01:00–04:00 UTC in a UTC-5 host came out as
|
|
103
|
+
// [0,23], i.e. almost the entire day at peak rates).
|
|
104
|
+
if (endMinutes <= 0) {
|
|
105
|
+
startMinutes += 1440
|
|
106
|
+
endMinutes += 1440
|
|
107
|
+
} else if (startMinutes >= 1440) {
|
|
108
|
+
startMinutes -= 1440
|
|
109
|
+
endMinutes -= 1440
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
if (startMinutes >= 0 && endMinutes <= 1440) {
|
|
99
113
|
out.push([round(startMinutes / 60), round(endMinutes / 60)])
|
|
100
114
|
continue
|
|
101
115
|
}
|
|
102
|
-
// The shifted window crosses midnight, so it becomes two ranges.
|
|
103
|
-
const wrap = (minutes) => ((minutes % 1440) + 1440) % 1440
|
|
104
116
|
out.push([round(wrap(startMinutes) / 60), 24])
|
|
105
117
|
out.push([0, round(wrap(endMinutes) / 60)])
|
|
106
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-usage-badge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DeepSeek Harness 的用量徽标插件:侧边栏底部一行显示当天的 token 总量与估算费用,点开可看 24 小时 / 近 7 日 / 近 30 日 / 近 12 个月用量,按 provider 筛选,并按 DeepSeek 官方定价与中国法定节假日计价。A usage badge plugin for DeepSeek Harness: today's tokens and estimated spend in the sidebar, with 24h / 7d / 30d / 12-month charts, official DeepSeek pricing and Chinese public holidays.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"docs"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node test/
|
|
42
|
+
"test": "node test/all.mjs",
|
|
43
43
|
"test:smoke": "node test/smoke.mjs",
|
|
44
44
|
"prepublishOnly": "npm test"
|
|
45
45
|
},
|