dsh-all-usage 1.0.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/client.js ADDED
@@ -0,0 +1,1063 @@
1
+ // dsh-all-usage 插件 Client 半(永久版,浏览器 bundle)
2
+ // 客户端模块工厂格式:window.__ModuleLoader__.load({ id, factory })
3
+ window.__ModuleLoader__.load({
4
+ id: "dsh-all-usage",
5
+ factory: (require) => {
6
+ var module = { exports: {} };
7
+ var exports = module.exports;
8
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
+ const React = require("react");
10
+
11
+ function pad2(n) {
12
+ return String(n).padStart(2, '0')
13
+ }
14
+ function fmtDate(d, utc) {
15
+ const year = utc ? d.getUTCFullYear() : d.getFullYear()
16
+ const month = utc ? d.getUTCMonth() : d.getMonth()
17
+ const day = utc ? d.getUTCDate() : d.getDate()
18
+ return year + '-' + pad2(month + 1) + '-' + pad2(day)
19
+ }
20
+ function shiftCalendarDate(d, days, utc) {
21
+ if (utc) return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + days))
22
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + days)
23
+ }
24
+ function trim1(v) {
25
+ return String(Math.round(v * 10) / 10)
26
+ }
27
+ function fmtCompact(n) {
28
+ if (typeof n !== 'number' || !Number.isFinite(n)) return '0'
29
+ if (n < 1000) return String(n)
30
+ if (n < 1000000) return trim1(n / 1000) + 'k'
31
+ if (n < 1000000000) return trim1(n / 1000000) + 'M'
32
+ return trim1(n / 1000000000) + 'B'
33
+ }
34
+ function rateOf(input, cacheRead) {
35
+ const denom = input + cacheRead
36
+ if (denom <= 0) return 0
37
+ return (cacheRead / denom) * 100
38
+ }
39
+ function LineIcon(props) {
40
+ const size = props.size || 16
41
+ const base = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' }
42
+ const paths = {
43
+ edit: [React.createElement('path', { key: 'a', d: 'M12.2 3.4l2.4 2.4M4 16l2.8-.6L15 7.2a1.7 1.7 0 0 0-2.4-2.4L4.4 13z', ...base })],
44
+ export: [React.createElement('path', { key: 'a', d: 'M12 3v11M8 7l4-4 4 4M5 13v5h14v-5', ...base })],
45
+ refresh: [React.createElement('path', { key: 'a', d: 'M19 9a7 7 0 1 0 1.1 5.2M19 4v5h-5', ...base })],
46
+ close: [React.createElement('path', { key: 'a', d: 'M6 6l12 12M18 6L6 18', ...base })],
47
+ chart: [React.createElement('path', { key: 'a', d: 'M4 19V5M4 19h16M7 15l3-4 3 2 5-7', ...base })],
48
+ cache: [React.createElement('path', { key: 'a', d: 'M12 4l7 4-7 4-7-4 7-4zM5 12l7 4 7-4M5 16l7 4 7-4', ...base })],
49
+ wallet: [React.createElement('path', { key: 'a', d: 'M4 7.5A2.5 2.5 0 0 1 6.5 5H18v14H6.5A2.5 2.5 0 0 1 4 16.5zM4 8h14M14 13h.01', ...base })],
50
+ clock: [React.createElement('path', { key: 'a', d: 'M12 6v6l4 2M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0z', ...base })],
51
+ folder: [React.createElement('path', { key: 'a', d: 'M3.5 7.5h6l2 2h9v8.5a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z', ...base })],
52
+ language: [React.createElement('circle', { key: 'a', cx: 12, cy: 12, r: 8, ...base }), React.createElement('path', { key: 'b', d: 'M4 12h16M12 4c2.1 2.2 3.2 4.9 3.2 8S14.1 17.8 12 20M12 4C9.9 6.2 8.8 8.9 8.8 12s1.1 5.8 3.2 8', ...base })],
53
+ chevron: [React.createElement('path', { key: 'a', d: 'M7 10l5 5 5-5', ...base })],
54
+ check: [React.createElement('path', { key: 'a', d: 'M5 12.5l4.2 4.1L19 7.3', ...base })],
55
+ calendar: [React.createElement('path', { key: 'a', d: 'M6 4v3M18 4v3M4 9h16M5 6h14a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z', ...base })],
56
+ }
57
+ return React.createElement('svg', { className: 'uh-line-icon ' + (props.className || ''), width: size, height: size, viewBox: '0 0 24 24', 'aria-hidden': true }, paths[props.name] || paths.chart)
58
+ }
59
+ function chineseMagnitude(n, language) {
60
+ if (language === 'en' || typeof n !== 'number' || !Number.isFinite(n) || n < 10000) return ''
61
+ const value = n >= 100000000 ? n / 100000000 : n / 10000
62
+ const rounded = Math.round(value * 1000) / 1000
63
+ return String(rounded) + (n >= 100000000 ? '亿' : '万')
64
+ }
65
+ function valueWithMagnitude(value, raw, language) {
66
+ const magnitude = chineseMagnitude(raw, language)
67
+ return React.createElement(React.Fragment, null, value, magnitude ? React.createElement('span', { className: 'uh-unit' }, magnitude) : null)
68
+ }
69
+ function money(currency, n, language) {
70
+ if (n === null || n === undefined) return '—'
71
+ const sym = currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : currency + ' '
72
+ return sym + n.toLocaleString(language === 'en' ? 'en-US' : 'zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
73
+ }
74
+ function humanDate(date, language) {
75
+ const parts = date.split('-')
76
+ const utc = language === 'en'
77
+ const d = utc ? new Date(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))) : new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
78
+ const monthIndex = utc ? d.getUTCMonth() : d.getMonth()
79
+ const weekIndex = utc ? d.getUTCDay() : d.getDay()
80
+ if (language === 'en') {
81
+ const month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][monthIndex]
82
+ const week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][weekIndex]
83
+ return month + ' ' + Number(parts[2]) + ', ' + parts[0] + ' (' + week + ', UTC)'
84
+ }
85
+ const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][weekIndex]
86
+ return parts[0] + '年' + Number(parts[1]) + '月' + Number(parts[2]) + '日 ' + week
87
+ }
88
+ function monthLabel(year, month, language) {
89
+ if (language === 'en') {
90
+ const label = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month]
91
+ return month === 0 ? year + ' ' + label : label
92
+ }
93
+ return month === 0 ? year + '年1月' : (month + 1) + '月'
94
+ }
95
+ function levelOf(count) {
96
+ if (count >= 10) return 4
97
+ if (count >= 6) return 3
98
+ if (count >= 3) return 2
99
+ if (count >= 1) return 1
100
+ return 0
101
+ }
102
+ const GH_GREEN = '#2ea043'
103
+ const LEVEL_PCT = [20, 45, 70, 96]
104
+ function cellBg(level) {
105
+ if (level <= 0) return 'var(--dsw-alias-bg-layer-2)'
106
+ return 'color-mix(in srgb, ' + GH_GREEN + ' ' + LEVEL_PCT[level - 1] + '%, var(--dsw-alias-bg-layer-2))'
107
+ }
108
+ function wsColor(i) {
109
+ return 'hsl(' + ((i * 137) % 360) + ', 70%, 55%)'
110
+ }
111
+ function rangeAgg(stats, range, utc) {
112
+ if (stats === null) return { totals: { turns: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }, perWs: [], perModel: [] }
113
+ if (range === 'all') return { totals: stats.totals, perWs: stats.perWorkspace, perModel: stats.perModel || [] }
114
+ const days = utc && Array.isArray(stats.byDayUtc) ? stats.byDayUtc : (Array.isArray(stats.byDay) ? stats.byDay : [])
115
+ let cutoff
116
+ if (range === 'today') cutoff = fmtDate(new Date(), utc)
117
+ else if (range === '30d') cutoff = fmtDate(shiftCalendarDate(new Date(), -29, utc), utc)
118
+ else cutoff = fmtDate(shiftCalendarDate(new Date(), -89, utc), utc)
119
+ const t = { turns: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
120
+ const per = new Map()
121
+ const models = new Map()
122
+ for (const day of days) {
123
+ if (day.date < cutoff) continue
124
+ t.turns += day.turns
125
+ t.input += day.tokens.input
126
+ t.output += day.tokens.output
127
+ t.cacheRead += day.tokens.cacheRead
128
+ t.cacheWrite += day.tokens.cacheWrite
129
+ t.reasoning += day.tokens.reasoning
130
+ for (const w of day.byWorkspace) {
131
+ let p = per.get(w.workspaceId)
132
+ if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; per.set(w.workspaceId, p) }
133
+ p.input += w.input
134
+ p.output += w.output
135
+ p.cacheRead += w.cacheRead
136
+ p.cacheWrite += w.cacheWrite
137
+ p.reasoning += w.reasoning
138
+ }
139
+ for (const w of day.perWorkspace) {
140
+ let p = per.get(w.workspaceId)
141
+ if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; per.set(w.workspaceId, p) }
142
+ p.turns += w.turns
143
+ }
144
+ for (const m of (day.byModel || [])) {
145
+ let p = models.get(m.model)
146
+ if (p === undefined) { p = { model: m.model, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; models.set(m.model, p) }
147
+ p.calls += m.calls; p.input += m.input; p.output += m.output; p.cacheRead += m.cacheRead; p.cacheWrite += m.cacheWrite; p.reasoning += m.reasoning
148
+ }
149
+ }
150
+ return { totals: t, perWs: Array.from(per.values()), perModel: Array.from(models.values()) }
151
+ }
152
+ function modelParts(label, unknownProvider, unknownModel) {
153
+ const text = typeof label === 'string' ? label : ''
154
+ const cut = text.indexOf(' / ')
155
+ return cut >= 0 ? { provider: text.slice(0, cut) || unknownProvider, model: text.slice(cut + 3) || unknownModel } : { provider: unknownProvider, model: text || unknownModel }
156
+ }
157
+ function aggregateModelRows(rows, view, unknownProvider, unknownModel) {
158
+ if (view === 'route') return rows.slice()
159
+ const grouped = new Map()
160
+ for (const row of rows) {
161
+ const parts = modelParts(row.model, unknownProvider, unknownModel)
162
+ const key = view === 'model' ? parts.model : parts.provider
163
+ let item = grouped.get(key)
164
+ if (item === undefined) { item = { model: key, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; grouped.set(key, item) }
165
+ item.calls += row.calls; item.input += row.input; item.output += row.output; item.cacheRead += row.cacheRead; item.cacheWrite += row.cacheWrite; item.reasoning += row.reasoning
166
+ }
167
+ return Array.from(grouped.values())
168
+ }
169
+ function streaks(dayMap, utc) {
170
+ const today = new Date()
171
+ let streak = 0
172
+ for (let i = 0; i < 371; i++) {
173
+ const d = shiftCalendarDate(today, -i, utc)
174
+ const day = dayMap.get(fmtDate(d, utc))
175
+ const active = day !== undefined && day.turns > 0
176
+ if (active) streak += 1
177
+ else if (i > 0) break
178
+ }
179
+ let best = 0
180
+ let run = 0
181
+ for (let i = 0; i < 371; i++) {
182
+ const d = shiftCalendarDate(today, -i, utc)
183
+ const day = dayMap.get(fmtDate(d, utc))
184
+ if (day !== undefined && day.turns > 0) {
185
+ run += 1
186
+ if (run > best) best = run
187
+ } else {
188
+ run = 0
189
+ }
190
+ }
191
+ return { streak, best }
192
+ }
193
+ // 数字滚动动画:首次从 0 滚动到目标值,之后直接同步目标值
194
+ function useCountUp(target, timer) {
195
+ const [state, setState] = React.useState({ value: 0, done: false })
196
+ React.useEffect(() => {
197
+ if (typeof target !== 'number' || !Number.isFinite(target) || target <= 0) {
198
+ setState({ value: 0, done: false })
199
+ return undefined
200
+ }
201
+ if (state.done) {
202
+ setState({ value: target, done: true })
203
+ return undefined
204
+ }
205
+ const start = Date.now()
206
+ const duration = 700
207
+ const stop = timer.interval(() => {
208
+ const t = Math.min(1, (Date.now() - start) / duration)
209
+ const eased = 1 - Math.pow(1 - t, 3)
210
+ if (t >= 1) {
211
+ stop()
212
+ setState({ value: target, done: true })
213
+ } else {
214
+ setState({ value: Math.round(target * eased), done: false })
215
+ }
216
+ }, 32)
217
+ return stop
218
+ }, [target])
219
+ return state.value
220
+ }
221
+
222
+ const CSS = `
223
+ .uh-page { display:flex; flex-direction:column; gap:14px; padding:2px 2px 28px; font-family:inherit; }
224
+ .uh-head { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
225
+ .uh-title { margin:0; font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); }
226
+ .uh-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
227
+ .uh-language-menu { position:relative; z-index:12; }
228
+ .uh-language-trigger { display:inline-flex; align-items:center; gap:6px; min-height:30px; padding:4px 9px 4px 10px; border:1px solid transparent; border-radius:15px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); color:var(--dsw-alias-label-primary); font:inherit; font-size:12px; font-weight:600; line-height:1; cursor:pointer; transition:border-color .15s ease, background-color .15s ease, transform .1s ease; }
229
+ .uh-language-trigger:hover, .uh-language-trigger.uh-open { border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 58%, var(--dsw-alias-border-l2)); background:color-mix(in srgb, var(--dsw-alias-brand-primary) 13%, var(--dsw-alias-bg-layer-1)); }
230
+ .uh-language-trigger:active { transform:scale(.96); }
231
+ .uh-language-label { min-width:26px; text-align:left; }
232
+ .uh-language-caret { color:var(--dsw-alias-label-secondary); transition:transform .18s ease; }
233
+ .uh-language-trigger.uh-open .uh-language-caret { transform:rotate(180deg); }
234
+ .uh-language-options { position:absolute; top:calc(100% + 7px); right:0; min-width:142px; padding:5px; border:1px solid var(--dsw-alias-border-l2); border-radius:12px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 14px 28px color-mix(in srgb, #000 24%, transparent); animation:uh-menu-in .16s ease both; }
235
+ .uh-language-option { display:flex; align-items:center; gap:8px; width:100%; min-height:32px; padding:6px 8px; border:0; border-radius:8px; background:transparent; color:var(--dsw-alias-label-primary); font:inherit; font-size:12px; text-align:left; cursor:pointer; transition:background-color .14s ease, color .14s ease; }
236
+ .uh-language-option:hover, .uh-language-option:focus-visible { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, var(--dsw-alias-bg-layer-2)); outline:0; }
237
+ .uh-language-option.uh-on { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 19%, var(--dsw-alias-bg-layer-2)); color:var(--dsw-alias-label-primary); font-weight:600; }
238
+ .uh-language-option-check { margin-left:auto; color:var(--dsw-alias-brand-primary); }
239
+ .uh-range { display:inline-flex; border:1px solid var(--dsw-alias-border-l2); border-radius:8px; overflow:hidden; }
240
+ .uh-range button { border:0; background:transparent; color:var(--dsw-alias-label-secondary); padding:4px 12px; font-size:12px; cursor:pointer; font-family:inherit; transition:background-color .15s ease, color .15s ease; }
241
+ .uh-range button + button { border-left:1px solid var(--dsw-alias-border-l2); }
242
+ .uh-range button.uh-on { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, var(--dsw-alias-bg-layer-2)); color:var(--dsw-alias-label-primary); font-weight:600; }
243
+ .uh-refresh { border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-primary); border-radius:8px; padding:4px 12px; font-size:12px; cursor:pointer; font-family:inherit; transition:border-color .15s ease, color .15s ease, transform .1s ease; }
244
+ .uh-refresh:hover { border-color:var(--dsw-alias-brand-primary); }
245
+ .uh-refresh:active, .uh-chip:active, .uh-range button:active { transform:scale(.96); }
246
+ .uh-alias-panel-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; font-size:13px; font-weight:600; color:var(--dsw-alias-label-primary); }
247
+ .uh-alias-close { border:0; background:transparent; color:var(--dsw-alias-label-secondary); font-size:12px; cursor:pointer; font-family:inherit; padding:0; transition:color .15s ease; }
248
+ .uh-alias-close:hover { color:var(--dsw-alias-brand-primary); }
249
+ .uh-alias-list { display:grid; grid-template-columns:repeat(auto-fill, minmax(250px, 1fr)); gap:8px 16px; max-height:240px; overflow-y:auto; }
250
+ .uh-alias-item { display:flex; align-items:center; gap:8px; min-width:0; }
251
+ .uh-alias-folder { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; color:var(--dsw-alias-label-secondary); }
252
+ .uh-alias-input { flex:none; width:150px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); border-radius:6px; padding:3px 8px; font-size:12px; font-family:inherit; outline:none; transition:border-color .15s ease; }
253
+ .uh-alias-input:focus { border-color:var(--dsw-alias-brand-primary); }
254
+ .uh-alias-panel-foot { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:10px; padding-top:10px; border-top:1px solid var(--dsw-alias-border-l1); }
255
+ .uh-alias-ok { border:1px solid var(--dsw-alias-brand-primary); background:color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:12px; padding:3px 12px; cursor:pointer; font-family:inherit; flex:none; transition:transform .1s ease; }
256
+ .uh-alias-ok:active { transform:scale(.96); }
257
+ .uh-anim-panel { animation:uh-panel-in .28s ease both; }
258
+ .uh-progress { font-size:12px; color:var(--dsw-alias-label-secondary); display:flex; align-items:center; gap:10px; }
259
+ .uh-bar { flex:1; height:6px; border-radius:3px; background:var(--dsw-alias-bg-layer-2); overflow:hidden; max-width:340px; }
260
+ .uh-fill { height:100%; background:var(--dsw-alias-brand-primary); border-radius:3px; transition:width .3s ease; }
261
+ .uh-cards { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:10px; }
262
+ .uh-card { background:var(--dsw-alias-bg-layer-1); border:1px solid var(--dsw-alias-border-l1); border-radius:12px; padding:12px 14px; display:flex; flex-direction:column; gap:6px; min-height:86px; animation:uh-card-in .45s ease both; transition:transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
263
+ .uh-card:hover { transform:translateY(-2px); border-color:var(--dsw-alias-border-l2); box-shadow:0 6px 18px rgba(0,0,0,.10); }
264
+ .uh-card-label { font-size:12px; color:var(--dsw-alias-label-secondary); }
265
+ .uh-card-value { font-size:20px; font-weight:650; color:var(--dsw-alias-label-primary); line-height:1.2; }
266
+ .uh-card-sub { font-size:11px; color:var(--dsw-alias-label-secondary); line-height:1.55; }
267
+ .uh-wsbars { display:flex; flex-direction:column; gap:6px; margin-top:2px; }
268
+ .uh-wsbar { display:flex; flex-direction:column; gap:3px; cursor:pointer; padding:2px 6px; margin:0 -6px; border-radius:8px; transition:background-color .15s ease; }
269
+ .uh-wsbar:hover { background:var(--dsw-alias-bg-layer-2); }
270
+ .uh-wsbar.uh-sel { outline:1px solid var(--dsw-alias-brand-primary); }
271
+ .uh-wsbar-top { display:flex; align-items:center; gap:6px; min-width:0; }
272
+ .uh-wsbar-title { font-size:12px; color:var(--dsw-alias-label-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0; }
273
+ .uh-wsbar-num { font-size:11px; font-variant-numeric:tabular-nums; color:var(--dsw-alias-label-secondary); flex:none; }
274
+ .uh-panel { background:var(--dsw-alias-bg-layer-1); border:1px solid var(--dsw-alias-border-l1); border-radius:12px; padding:14px; }
275
+ .uh-hm-head { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; margin-bottom:10px; }
276
+ .uh-chips { display:flex; flex-wrap:wrap; gap:6px; }
277
+ .uh-chip { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); border-radius:999px; padding:2px 10px; font-size:11px; cursor:pointer; font-family:inherit; max-width:190px; transition:border-color .15s ease, background-color .15s ease, color .15s ease, transform .1s ease; }
278
+ .uh-chip .uh-chip-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
279
+ .uh-chip.uh-on { border-color:var(--dsw-alias-brand-primary); background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
280
+ .uh-dot { width:8px; height:8px; border-radius:50%; flex:none; }
281
+ .uh-legend { display:flex; align-items:center; gap:4px; font-size:11px; color:var(--dsw-alias-label-secondary); }
282
+ .uh-legend .uh-cell { width:10px; height:10px; border-radius:2px; animation:none; }
283
+ .uh-hm-scroll { overflow-x:auto; padding-bottom:2px; }
284
+ .uh-months { position:relative; height:16px; margin-left:30px; width:calc(100% - 30px); min-width:686px; font-size:10px; color:var(--dsw-alias-label-secondary); }
285
+ .uh-months span { position:absolute; top:0; }
286
+ .uh-hm-body { display:flex; gap:6px; min-width:0; }
287
+ .uh-wdays { display:grid; grid-template-rows:repeat(7,10px); gap:3px; font-size:10px; color:var(--dsw-alias-label-secondary); text-align:right; width:24px; }
288
+ .uh-wdays span { line-height:10px; }
289
+ .uh-grid { flex:1 1 auto; min-width:686px; display:grid; grid-auto-flow:column; grid-template-columns:repeat(53,minmax(10px,1fr)); grid-template-rows:repeat(7,minmax(10px,auto)); gap:3px; }
290
+ .uh-cell { width:100%; height:auto; min-width:10px; aspect-ratio:1; border-radius:2px; background:var(--dsw-alias-bg-layer-2); animation:uh-cell-in .45s ease both; transition:transform .12s ease, box-shadow .12s ease; }
291
+ .uh-cell:hover { transform:scale(1.35); box-shadow:0 1px 6px rgba(0,0,0,.28); position:relative; z-index:2; }
292
+ .uh-tip { position:fixed; z-index:1200; background:var(--dsw-alias-bg-overlay); border:1px solid var(--dsw-alias-border-l2); border-radius:10px; padding:10px 12px; box-shadow:0 8px 24px rgba(0,0,0,.18); pointer-events:auto; min-width:200px; max-width:290px; animation:uh-tip-in .16s ease both; }
293
+ .uh-tip-date { font-size:12px; font-weight:600; color:var(--dsw-alias-label-primary); margin-bottom:6px; }
294
+ .uh-tip-row { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--dsw-alias-label-primary); padding:3px 6px; margin:0 -6px; border-radius:6px; cursor:pointer; transition:background-color .12s ease; }
295
+ .uh-tip-row:hover { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); }
296
+ .uh-tip-row .uh-n { margin-left:auto; font-variant-numeric:tabular-nums; color:var(--dsw-alias-label-secondary); }
297
+ .uh-tip-tokens { font-size:11px; color:var(--dsw-alias-label-secondary); margin-top:6px; border-top:1px solid var(--dsw-alias-border-l1); padding-top:6px; }
298
+ .uh-tbl-title { font-size:13px; font-weight:600; color:var(--dsw-alias-label-primary); margin:0 0 10px; }
299
+ .uh-tbl-scroll { overflow-x:auto; }
300
+ .uh-hrow, .uh-row { display:grid; grid-template-columns:minmax(160px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .8fr 1fr; gap:8px; align-items:center; min-width:780px; padding:7px 10px; border-radius:8px; font-size:12px; }
301
+ .uh-model-hrow, .uh-model-row { display:grid; grid-template-columns:minmax(190px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .8fr; gap:8px; align-items:center; min-width:780px; padding:7px 10px; border-radius:8px; font-size:12px; }
302
+ .uh-hrow { color:var(--dsw-alias-label-secondary); font-size:11px; }
303
+ .uh-row { cursor:pointer; border:1px solid transparent; transition:background-color .15s ease, border-color .15s ease; }
304
+ .uh-row:hover { background:var(--dsw-alias-bg-layer-2); }
305
+ .uh-row.uh-sel { border-color:var(--dsw-alias-brand-primary); }
306
+ .uh-num { text-align:right; font-variant-numeric:tabular-nums; color:var(--dsw-alias-label-primary); }
307
+ .uh-hrow .uh-num { color:var(--dsw-alias-label-secondary); }
308
+ .uh-ws-title { color:var(--dsw-alias-label-primary); font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
309
+ .uh-ws-path { color:var(--dsw-alias-label-secondary); font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
310
+ .uh-barwrap { height:5px; border-radius:3px; background:var(--dsw-alias-bg-layer-2); overflow:hidden; margin-top:3px; }
311
+ .uh-barwrap.uh-bar-thin { height:3px; margin-top:1px; }
312
+ .uh-barfill { height:100%; border-radius:3px; transform-origin:left center; animation:uh-bar-grow .7s cubic-bezier(.22,.61,.36,1) both; transition:width .5s cubic-bezier(.22,.61,.36,1); }
313
+ .uh-empty { color:var(--dsw-alias-label-secondary); font-size:12px; text-align:center; padding:26px 0; }
314
+ .uh-note { font-size:11px; color:var(--dsw-alias-label-secondary); line-height:1.6; }
315
+ .uh-side-entry { width:100%; border:0; background:transparent; color:var(--dsw-alias-label-secondary); border-radius:8px; min-height:36px; padding:7px 10px; display:flex; align-items:center; gap:9px; font:inherit; font-size:13px; cursor:pointer; text-align:left; }
316
+ .uh-side-entry:hover { background:var(--dsw-alias-interactive-bg-hover); color:var(--dsw-alias-label-primary); }
317
+ .uh-side-entry-icon { width:18px; text-align:center; flex:none; font-size:15px; }
318
+ .uh-side-entry-label { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
319
+ .uh-side-modal { position:fixed; inset:0; z-index:1100; background:color-mix(in srgb, #000 44%, transparent); display:flex; align-items:stretch; justify-content:center; padding:26px; }
320
+ .uh-side-dialog { width:min(1120px, 100%); overflow:auto; background:var(--dsw-alias-bg-base); border:1px solid var(--dsw-alias-border-l2); border-radius:14px; box-shadow:0 18px 52px rgba(0,0,0,.35); padding:18px; }
321
+ .uh-side-dialog-head { display:flex; justify-content:flex-end; margin-bottom:8px; }
322
+ @media (max-width: 640px) { .uh-side-modal { padding:0; } .uh-side-dialog { border-radius:0; border:0; padding:14px; } }
323
+ /* iOS-style dashboard: grouped surfaces, tactile controls, and an elevated sheet. */
324
+ .uh-page { gap:18px; max-width:1160px; margin:0 auto; padding:4px 2px 34px; font-family:-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif; }
325
+ .uh-head { position:sticky; top:-18px; z-index:5; margin:0 -2px; padding:18px 2px 14px; background:color-mix(in srgb, var(--dsw-alias-bg-base) 88%, transparent); backdrop-filter:blur(18px) saturate(150%); border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 76%, transparent); }
326
+ .uh-title { font-size:22px; line-height:1.2; font-weight:700; letter-spacing:0; }
327
+ .uh-actions { gap:8px; }
328
+ .uh-range { padding:2px; gap:2px; border:0; border-radius:9px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent); overflow:visible; }
329
+ .uh-range button, .uh-range button + button { min-height:28px; border:0; border-radius:7px; padding:4px 10px; }
330
+ .uh-range button.uh-on { background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 3px rgba(0,0,0,.16); }
331
+ .uh-refresh { min-height:30px; border:0; border-radius:15px; padding:5px 12px; display:inline-flex; align-items:center; justify-content:center; gap:6px; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, var(--dsw-alias-bg-layer-1)); color:var(--dsw-alias-brand-primary); font-weight:600; }
332
+ .uh-line-icon { flex:none; }
333
+ .uh-icon-button { width:30px; padding:0; }
334
+ .uh-refresh:hover { border:0; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, var(--dsw-alias-bg-layer-1)); }
335
+ .uh-progress { padding:10px 12px; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 9%, var(--dsw-alias-bg-layer-1)); border:0; border-radius:12px; }
336
+ .uh-cards { grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:10px; border:0; border-radius:0; overflow:visible; background:transparent; }
337
+ .uh-card { min-height:84px; padding:12px 14px; gap:4px; border:0; border-radius:18px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 2px rgba(0,0,0,.07), 0 8px 22px rgba(0,0,0,.05); animation:none; }
338
+ .uh-card:first-child { border:0; background:color-mix(in srgb, #0a84ff 15%, var(--dsw-alias-bg-layer-1)); }
339
+ .uh-card:nth-child(2) { background:color-mix(in srgb, #30d158 13%, var(--dsw-alias-bg-layer-1)); }
340
+ .uh-card:nth-child(3) { background:color-mix(in srgb, #ff9f0a 14%, var(--dsw-alias-bg-layer-1)); }
341
+ .uh-card:hover { transform:translateY(-2px); box-shadow:0 12px 28px rgba(0,0,0,.12); }
342
+ .uh-card-label { display:flex; align-items:center; gap:6px; font-size:12px; font-weight:600; letter-spacing:0; }
343
+ .uh-ios-summary-label, .uh-section-title, .uh-title-with-icon { display:flex; align-items:center; gap:7px; }
344
+ .uh-section-title { margin-bottom:12px; color:var(--dsw-alias-label-primary); font-size:14px; font-weight:650; }
345
+ /* Raise the complete reading scale without changing the data grid geometry. */
346
+ .uh-page { font-size:14px; }
347
+ .uh-range button, .uh-refresh { font-size:13px; }
348
+ .uh-card-label, .uh-ios-summary-label { font-size:13px; }
349
+ .uh-card-sub, .uh-ios-summary-caption, .uh-note { font-size:12px; }
350
+ .uh-hrow, .uh-row, .uh-model-hrow, .uh-model-row { font-size:13px; }
351
+ .uh-tbl-title { font-size:15px; }
352
+ .uh-num { font-variant-numeric:tabular-nums; }
353
+ .uh-card-value { font-size:23px; font-weight:700; letter-spacing:0; }
354
+ .uh-card-sub { font-size:11px; line-height:1.45; }
355
+ .uh-panel { padding:16px; border:0; border-radius:18px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 2px rgba(0,0,0,.06), 0 6px 18px rgba(0,0,0,.04); }
356
+ .uh-hm-head { margin-bottom:12px; }
357
+ .uh-chip { border:0; border-radius:14px; padding:5px 10px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent); }
358
+ .uh-chip.uh-on { border:0; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); color:var(--dsw-alias-brand-primary); }
359
+ .uh-hrow, .uh-row, .uh-model-hrow, .uh-model-row { border-radius:10px; }
360
+ .uh-hrow, .uh-model-hrow { position:sticky; top:66px; z-index:2; background:var(--dsw-alias-bg-layer-1); border-bottom:1px solid var(--dsw-alias-border-l1); }
361
+ .uh-row, .uh-model-row { padding-top:9px; padding-bottom:9px; }
362
+ .uh-row:nth-child(even) { background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 52%, transparent); }
363
+ .uh-side-entry { min-height:40px; border:0; border-radius:12px; padding:8px 10px; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 9%, transparent); color:var(--dsw-alias-brand-primary); font-weight:600; }
364
+ .uh-side-entry:hover { border:0; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 17%, transparent); }
365
+ .uh-side-entry-icon { color:var(--dsw-alias-brand-primary); font-weight:700; }
366
+ .uh-side-modal { align-items:flex-end; padding:0; background:rgba(0,0,0,.34); backdrop-filter:blur(8px); }
367
+ .uh-side-dialog { width:min(1260px, 100%); max-height:calc(100vh - 44px); border:0; border-radius:24px 24px 0 0; padding:22px 24px 28px; background:color-mix(in srgb, var(--dsw-alias-bg-base) 94%, transparent); box-shadow:0 -10px 44px rgba(0,0,0,.25); }
368
+ .uh-side-dialog-head { position:sticky; top:-22px; z-index:8; justify-content:center; height:22px; margin:-22px -24px 8px; padding:8px 24px; background:color-mix(in srgb, var(--dsw-alias-bg-base) 94%, transparent); border:0; }
369
+ .uh-side-dialog-head::before { content:""; width:36px; height:5px; border-radius:3px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 24%, transparent); }
370
+ .uh-side-dialog-head .uh-refresh { position:absolute; right:20px; top:7px; min-height:28px; background:transparent; }
371
+ .uh-close-button { width:30px; padding:0; font-size:22px; line-height:1; color:var(--dsw-alias-label-secondary); }
372
+ .uh-close-button:hover { color:var(--dsw-alias-label-primary); background:color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent); }
373
+ @media (max-width:640px) { .uh-page { gap:14px; padding-bottom:20px; } .uh-head { position:static; padding:4px 0 10px; } .uh-title { font-size:20px; } .uh-side-dialog { max-height:calc(100vh - 8px); border-radius:20px 20px 0 0; padding:18px 14px 24px; } .uh-side-dialog-head { top:-18px; margin:-18px -14px 8px; padding:7px 14px; } .uh-card-value { font-size:22px; } }
374
+ /* Navigation separates the dashboard into three focused iOS-style surfaces. */
375
+ .uh-ios-tabs { display:grid; grid-template-columns:repeat(3, 1fr); gap:4px; padding:4px; border-radius:14px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 9%, transparent); }
376
+ .uh-ios-tab { min-height:32px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
377
+ .uh-ios-tab.uh-on { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 4px rgba(0,0,0,.16); }
378
+ .uh-ios-summary { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; background:transparent; box-shadow:none; }
379
+ .uh-ios-summary-total, .uh-ios-summary-cache { min-height:104px; padding:16px 20px; border-radius:22px; display:flex; flex-direction:column; justify-content:center; box-shadow:0 14px 32px rgba(0,0,0,.08); }
380
+ .uh-ios-summary-total { background:color-mix(in srgb, #0a84ff 18%, var(--dsw-alias-bg-layer-1)); }
381
+ .uh-ios-summary-cache { background:color-mix(in srgb, #30d158 17%, var(--dsw-alias-bg-layer-1)); }
382
+ .uh-ios-summary-label { font-size:13px; font-weight:600; color:var(--dsw-alias-label-secondary); }
383
+ .uh-ios-summary-value { margin-top:4px; font-size:32px; line-height:1; font-weight:750; letter-spacing:0; color:var(--dsw-alias-label-primary); }
384
+ .uh-unit { margin-left:6px; color:var(--dsw-alias-label-secondary); font-size:.4em; font-weight:650; white-space:nowrap; vertical-align:baseline; }
385
+ .uh-wsbar-num .uh-unit { font-size:.78em; margin-left:3px; }
386
+ .uh-ios-summary-caption { margin-top:8px; font-size:12px; color:var(--dsw-alias-label-secondary); }
387
+ .uh-token-semantics { display:flex; align-items:flex-start; gap:8px; padding:10px 12px; border-radius:12px; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 8%, var(--dsw-alias-bg-layer-1)); color:var(--dsw-alias-label-secondary); font-size:12px; line-height:1.55; }
388
+ .uh-token-semantics .uh-line-icon { margin-top:1px; color:var(--dsw-alias-brand-primary); }
389
+ .uh-ios-summary-meta { display:grid; align-content:center; grid-template-columns:1fr auto; gap:7px 12px; padding:20px 22px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 64%, transparent); font-size:12px; color:var(--dsw-alias-label-secondary); }
390
+ .uh-ios-summary-meta strong { color:var(--dsw-alias-label-primary); font-size:15px; font-variant-numeric:tabular-nums; }
391
+ .uh-ios-list-panel { min-height:360px; }
392
+ /* Each detail table owns its scrolling and sticky header; sections must not overlap in the page scroll. */
393
+ .uh-tbl-scroll { max-height:360px; overflow:auto; border-radius:12px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent); }
394
+ .uh-hrow, .uh-model-hrow { position:sticky; top:0; z-index:3; border-bottom:1px solid var(--dsw-alias-border-l1); box-shadow:0 1px 0 color-mix(in srgb, var(--dsw-alias-bg-base) 70%, transparent); }
395
+ .uh-row, .uh-model-row { min-height:48px; border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 72%, transparent); }
396
+ .uh-row:last-child, .uh-model-row:last-child { border-bottom:0; }
397
+ @media (max-width:640px) { .uh-tbl-scroll { max-height:300px; border-radius:10px; } }
398
+ @media (max-width:640px) { .uh-hm-body { min-width:720px; } .uh-ios-summary { grid-template-columns:1fr; } .uh-ios-summary-total, .uh-ios-summary-cache { padding:18px; min-height:0; border-radius:18px; } .uh-ios-summary-value { font-size:31px; } }
399
+ @keyframes uh-cell-in { from { opacity:0; transform:scale(.4); } to { opacity:1; transform:scale(1); } }
400
+ @keyframes uh-glow { 0% { box-shadow:0 0 0 0 rgba(46,160,67,.5); } 70% { box-shadow:0 0 0 5px rgba(46,160,67,0); } 100% { box-shadow:0 0 0 0 rgba(46,160,67,0); } }
401
+ @keyframes uh-card-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
402
+ @keyframes uh-bar-grow { from { transform:scaleX(0); } to { transform:scaleX(1); } }
403
+ @keyframes uh-panel-in { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
404
+ @keyframes uh-menu-in { from { opacity:0; transform:translateY(-4px) scale(.97); } to { opacity:1; transform:translateY(0) scale(1); } }
405
+ @keyframes uh-tip-in { from { opacity:0; } to { opacity:1; } }
406
+ @media (prefers-reduced-motion: reduce) {
407
+ .uh-cell, .uh-card, .uh-barfill, .uh-anim-panel, .uh-tip, .uh-language-options { animation:none !important; }
408
+ .uh-card, .uh-cell, .uh-barfill, .uh-fill, .uh-refresh, .uh-chip, .uh-row, .uh-tip-row, .uh-language-trigger, .uh-language-caret, .uh-language-option { transition:none !important; }
409
+ }
410
+ `
411
+ const cssTagId = "dsh-all-usage/styles.css"
412
+ if (typeof document !== "undefined") {
413
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(cssTagId) + "]")
414
+ if (tag === null) {
415
+ tag = document.createElement("style")
416
+ tag.dataset.plugin = "dsh-all-usage"
417
+ tag.dataset.pluginCss = cssTagId
418
+ document.head.appendChild(tag)
419
+ }
420
+ tag.textContent = CSS
421
+ }
422
+
423
+ // 与 Host 半的数据接口(webServer 路由)
424
+ const getStats = () => fetch('/api/all-usage', { headers: { accept: 'application/json' } }).then((r) => {
425
+ if (!r.ok) throw new Error('HTTP ' + r.status)
426
+ return r.json()
427
+ })
428
+ const getBalance = (force, requestToken) => fetch('/api/all-usage/balance' + (force ? '?force=1' : ''), { headers: { accept: 'application/json', 'x-all-usage-request-token': requestToken } }).then((r) => {
429
+ if (!r.ok) throw new Error('HTTP ' + r.status)
430
+ return r.json()
431
+ })
432
+ const setAliasRpc = (workspaceId, alias, writeToken) => fetch('/api/all-usage/alias', {
433
+ method: 'POST',
434
+ headers: { 'content-type': 'application/json', 'x-all-usage-request-token': writeToken },
435
+ body: JSON.stringify({ workspaceId, alias }),
436
+ }).then((r) => {
437
+ if (!r.ok) throw new Error('HTTP ' + r.status)
438
+ return r.json()
439
+ })
440
+
441
+ const LANGUAGE_STORAGE_KEY = 'dsh-all-usage.language'
442
+ function storedLanguage() {
443
+ try { return window.localStorage.getItem(LANGUAGE_STORAGE_KEY) === 'en' ? 'en' : 'zh' } catch (_) { return 'zh' }
444
+ }
445
+ function persistLanguage(language) {
446
+ try { window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language) } catch (_) {}
447
+ }
448
+
449
+ function UsagePage(props) {
450
+ const timer = props.timerCtx
451
+ const language = props.language === 'en' ? 'en' : 'zh'
452
+ const tr = (zh, en) => language === 'en' ? en : zh
453
+ const [stats, setStats] = React.useState(null)
454
+ const [balance, setBalance] = React.useState(null)
455
+ const [range, setRange] = React.useState('90d')
456
+ const [modelView, setModelView] = React.useState('route')
457
+ const [wsFilter, setWsFilter] = React.useState(null)
458
+ const [hover, setHover] = React.useState(null)
459
+ const [aliasOpen, setAliasOpen] = React.useState(false)
460
+ const [aliasDrafts, setAliasDrafts] = React.useState({})
461
+ const [languageMenuOpen, setLanguageMenuOpen] = React.useState(false)
462
+ const languageMenuRef = React.useRef(null)
463
+ const setLanguage = (next) => { if (typeof props.onLanguageChange === 'function') props.onLanguageChange(next === 'en' ? 'en' : 'zh') }
464
+ const chooseLanguage = (next) => { setLanguage(next); setLanguageMenuOpen(false) }
465
+
466
+ React.useEffect(() => {
467
+ let alive = true
468
+ let scanDone = false
469
+ let requestToken = ''
470
+ const refreshBalance = (force) => {
471
+ if (requestToken === '') return
472
+ getBalance(force === true, requestToken).then((data) => {
473
+ if (alive && data) setBalance(data)
474
+ }, () => {})
475
+ }
476
+ const refreshStats = () => {
477
+ getStats().then((data) => {
478
+ if (!alive) return
479
+ if (data && data.scan) scanDone = !!data.scan.done
480
+ const nextToken = data && typeof data.requestToken === 'string' ? data.requestToken : ''
481
+ const tokenChanged = nextToken !== '' && nextToken !== requestToken
482
+ requestToken = nextToken
483
+ setStats(data)
484
+ if (tokenChanged) refreshBalance(false)
485
+ }, () => {})
486
+ }
487
+ refreshStats()
488
+ const fast = timer.interval(() => { if (!scanDone) refreshStats() }, 2000)
489
+ const slow = timer.interval(() => { if (scanDone) refreshStats() }, 15000)
490
+ const bal = timer.interval(() => { refreshBalance(false) }, 60000)
491
+ return () => { alive = false; fast(); slow(); bal() }
492
+ }, [])
493
+
494
+ React.useEffect(() => {
495
+ if (!languageMenuOpen || typeof document === 'undefined') return undefined
496
+ const closeLanguageMenu = (event) => {
497
+ if (languageMenuRef.current && !languageMenuRef.current.contains(event.target)) setLanguageMenuOpen(false)
498
+ }
499
+ document.addEventListener('pointerdown', closeLanguageMenu)
500
+ return () => document.removeEventListener('pointerdown', closeLanguageMenu)
501
+ }, [languageMenuOpen])
502
+
503
+ const onRefresh = () => {
504
+ getStats().then((data) => {
505
+ if (!data) return
506
+ setStats(data)
507
+ const requestToken = typeof data.requestToken === 'string' ? data.requestToken : ''
508
+ if (requestToken !== '') getBalance(true, requestToken).then((balanceData) => { if (balanceData) setBalance(balanceData) }, () => {})
509
+ }, () => {})
510
+ }
511
+ const toggleFilter = (id) => {
512
+ setWsFilter((prev) => (prev === id ? null : id))
513
+ }
514
+
515
+ const useUtc = language === 'en'
516
+ const agg = rangeAgg(stats, range, useUtc)
517
+ const animatedTotal = useCountUp(agg.totals.input + agg.totals.output + agg.totals.cacheRead + agg.totals.cacheWrite + agg.totals.reasoning, timer)
518
+ const animatedRate = useCountUp(Math.round(rateOf(agg.totals.input, agg.totals.cacheRead) * 10), timer)
519
+ const animatedTurns = useCountUp(agg.totals.turns, timer)
520
+
521
+ if (stats === null) {
522
+ return React.createElement('div', { className: 'uh-page' },
523
+ React.createElement('div', { className: 'uh-panel' }, React.createElement('div', { className: 'uh-empty' }, tr('正在加载用量统计…', 'Loading usage statistics…'))),
524
+ )
525
+ }
526
+
527
+ const scan = stats.scan || { done: true, started: true, scanned: 0, total: 0, failed: 0 }
528
+ const workspaces = Array.isArray(stats.workspaces) ? stats.workspaces : []
529
+ const aliases = stats.aliases && typeof stats.aliases === 'object' ? stats.aliases : {}
530
+ const wsById = new Map()
531
+ const wsIndex = new Map()
532
+ workspaces.forEach((w, i) => { wsById.set(w.id, w); wsIndex.set(w.id, i) })
533
+ const dayRows = useUtc && Array.isArray(stats.byDayUtc) ? stats.byDayUtc : (Array.isArray(stats.byDay) ? stats.byDay : [])
534
+ const dayMap = new Map()
535
+ for (const d of dayRows) dayMap.set(d.date, d)
536
+ const wsTitle = (id) => {
537
+ const alias = aliases[id]
538
+ if (typeof alias === 'string' && alias !== '') return alias
539
+ const meta = wsById.get(id)
540
+ return meta ? meta.title : tr('未知工作区', 'Unknown workspace')
541
+ }
542
+
543
+ const saveAlias = (wsId, value) => {
544
+ const requestToken = typeof stats.requestToken === 'string' ? stats.requestToken : ''
545
+ if (requestToken === '') return
546
+ setAliasRpc(wsId, String(value === undefined ? '' : value).trim(), requestToken).then((res) => {
547
+ if (res && res.ok && res.aliases) {
548
+ setStats((prev) => (prev === null ? prev : Object.assign({}, prev, { aliases: res.aliases })))
549
+ }
550
+ }, () => {})
551
+ }
552
+ const openAliasPanel = () => {
553
+ const drafts = {}
554
+ workspaces.forEach((w) => { drafts[w.id] = typeof aliases[w.id] === 'string' ? aliases[w.id] : '' })
555
+ setAliasDrafts(drafts)
556
+ setAliasOpen(true)
557
+ }
558
+ const saveAllAliases = () => {
559
+ for (const id of Object.keys(aliasDrafts)) {
560
+ const current = typeof aliases[id] === 'string' ? aliases[id] : ''
561
+ if (aliasDrafts[id] !== current) saveAlias(id, aliasDrafts[id])
562
+ }
563
+ setAliasOpen(false)
564
+ }
565
+
566
+ const totalTokens = agg.totals.input + agg.totals.output + agg.totals.cacheRead + agg.totals.cacheWrite + agg.totals.reasoning
567
+ const cacheRate = rateOf(agg.totals.input, agg.totals.cacheRead)
568
+ const st = streaks(dayMap, useUtc)
569
+
570
+ const today = new Date()
571
+ const todayKey = fmtDate(today, useUtc)
572
+ const todayWeekday = useUtc ? today.getUTCDay() : today.getDay()
573
+ const sunday = shiftCalendarDate(today, -todayWeekday, useUtc)
574
+ const start = shiftCalendarDate(sunday, -52 * 7, useUtc)
575
+ const cells = []
576
+ for (let i = 0; i < 53 * 7; i++) {
577
+ const d = shiftCalendarDate(start, i, useUtc)
578
+ cells.push({ date: fmtDate(d, useUtc), month: useUtc ? d.getUTCMonth() : d.getMonth(), year: useUtc ? d.getUTCFullYear() : d.getFullYear() })
579
+ }
580
+ const monthLabels = []
581
+ for (let j = 0; j < 53; j++) {
582
+ const first = cells[j * 7]
583
+ const prev = j > 0 ? cells[(j - 1) * 7] : null
584
+ if (prev === null || first.month !== prev.month) {
585
+ monthLabels.push({ left: (j * 100 / 53) + '%', text: monthLabel(first.year, first.month, language) })
586
+ }
587
+ }
588
+ const weekdayLabels = language === 'en' ? ['', 'Mon', '', 'Wed', '', 'Fri', ''] : ['', '周一', '', '周三', '', '周五', '']
589
+
590
+ const onEnter = (cell, ev) => {
591
+ setHover({ date: cell.date, x: ev.clientX, y: ev.clientY, day: dayMap.get(cell.date) })
592
+ }
593
+ const onMove = (cell, ev) => {
594
+ setHover((prev) => (prev !== null && prev.date === cell.date ? { date: prev.date, x: ev.clientX, y: ev.clientY, day: prev.day } : prev))
595
+ }
596
+ const onLeave = () => setHover(null)
597
+
598
+ const cellElements = cells.map((cell, i) => {
599
+ const day = dayMap.get(cell.date)
600
+ let count = 0
601
+ if (day !== undefined) {
602
+ if (wsFilter === null) count = day.turns
603
+ else {
604
+ const w = day.perWorkspace.find((x) => x.workspaceId === wsFilter)
605
+ if (w !== undefined) count = w.turns
606
+ }
607
+ }
608
+ const level = levelOf(count)
609
+ const dim = wsFilter !== null && day !== undefined && day.turns > 0 && count === 0
610
+ const isToday = cell.date === todayKey
611
+ const style = {
612
+ background: cellBg(level),
613
+ opacity: dim ? 0.22 : 1,
614
+ animationDelay: (i * 1.2) + 'ms',
615
+ }
616
+ if (isToday) style.animation = 'uh-cell-in .45s ease both, uh-glow 3s ease-in-out .7s infinite'
617
+ return React.createElement('div', {
618
+ key: cell.date,
619
+ className: 'uh-cell',
620
+ style,
621
+ onMouseEnter: (ev) => onEnter(cell, ev),
622
+ onMouseMove: (ev) => onMove(cell, ev),
623
+ onMouseLeave: onLeave,
624
+ })
625
+ })
626
+
627
+ let balanceValue = '—'
628
+ let balanceSub = tr('查询中…', 'Checking…')
629
+ if (balance !== null && balance !== undefined) {
630
+ if (balance.status === 'missing-key') {
631
+ balanceValue = tr('未配置', 'Not configured')
632
+ balanceSub = tr('在 设置 → 模型 中填写 DeepSeek API Key 后可见', 'Available after you enter a DeepSeek API key in Settings → Models')
633
+ } else if (balance.status === 'unavailable') {
634
+ balanceValue = tr('不可用', 'Unavailable')
635
+ balanceSub = balance.message || tr('DeepSeek 接口返回余额不可用', 'The DeepSeek API reported that balance information is unavailable')
636
+ } else if (balance.status === 'error') {
637
+ balanceValue = tr('查询失败', 'Lookup failed')
638
+ const detail = balance.detail ? (language === 'en' ? ' (' + String(balance.detail).slice(0, 90) + ')' : '(' + String(balance.detail).slice(0, 90) + ')') : ''
639
+ balanceSub = (balance.message || '') + detail + tr(' 点“刷新”重试', ' Click Refresh to try again')
640
+ } else if (balance.status === 'ok' && Array.isArray(balance.currencies) && balance.currencies.length > 0) {
641
+ const list = balance.currencies
642
+ const primary = list.find((c) => c.currency === 'CNY') || list[0]
643
+ const others = list.filter((c) => c !== primary)
644
+ balanceValue = money(primary.currency, primary.total, language)
645
+ let sub = primary.total !== null ? tr('赠送 ', 'Granted ') + money(primary.currency, primary.granted, language) + ' · ' + tr('充值 ', 'Top-up ') + money(primary.currency, primary.toppedUp, language) : ''
646
+ if (others.length > 0) sub += (sub ? ' | ' : '') + others.map((c) => money(c.currency, c.total, language)).join(' ')
647
+ balanceSub = sub
648
+ } else {
649
+ balanceValue = tr('无数据', 'No data')
650
+ balanceSub = ''
651
+ }
652
+ }
653
+
654
+ const card = (label, value, sub, delay, icon) => React.createElement('div', { className: 'uh-card', style: { animationDelay: (delay * 70) + 'ms' } },
655
+ React.createElement('div', { className: 'uh-card-label' }, icon ? React.createElement(LineIcon, { name: icon, size: 14 }) : null, label),
656
+ React.createElement('div', { className: 'uh-card-value' }, value),
657
+ React.createElement('div', { className: 'uh-card-sub' }, sub),
658
+ )
659
+
660
+ const wsTotal = (w) => w.input + w.output + w.cacheRead + w.cacheWrite + w.reasoning
661
+ const rows = agg.perWs.slice().sort((a, b) => wsTotal(b) - wsTotal(a))
662
+ const maxTotal = rows.length > 0 ? wsTotal(rows[0]) : 0
663
+
664
+ const tokenCardRows = rows.slice(0, 3).map((w) => {
665
+ const total = wsTotal(w)
666
+ const idx = wsIndex.get(w.workspaceId)
667
+ const color = wsColor(idx === undefined ? 0 : idx)
668
+ const selected = wsFilter === w.workspaceId
669
+ return React.createElement('div', {
670
+ key: w.workspaceId,
671
+ className: 'uh-wsbar' + (selected ? ' uh-sel' : ''),
672
+ onClick: () => toggleFilter(w.workspaceId),
673
+ },
674
+ React.createElement('div', { className: 'uh-wsbar-top' },
675
+ React.createElement('span', { className: 'uh-dot', style: { background: color } }),
676
+ React.createElement('span', { className: 'uh-wsbar-title' }, wsTitle(w.workspaceId)),
677
+ React.createElement('span', { className: 'uh-wsbar-num' }, valueWithMagnitude(fmtCompact(total), total, language)),
678
+ ),
679
+ React.createElement('div', { className: 'uh-barwrap uh-bar-thin' },
680
+ React.createElement('div', { className: 'uh-barfill', style: { width: maxTotal > 0 ? Math.max(2, (total / maxTotal) * 100) + '%' : '0%', background: color } }),
681
+ ),
682
+ )
683
+ })
684
+ const tokenCard = React.createElement('div', { className: 'uh-card', style: { animationDelay: '210ms' } },
685
+ React.createElement('div', { className: 'uh-card-label' }, React.createElement(LineIcon, { name: 'folder', size: 14 }), tr('各工作区总处理量', 'Total Tokens Processed by Workspace')),
686
+ rows.length === 0
687
+ ? React.createElement('div', { className: 'uh-empty', style: { padding: '8px 0' } }, tr('暂无数据', 'No data yet'))
688
+ : React.createElement('div', { className: 'uh-wsbars' },
689
+ tokenCardRows,
690
+ rows.length > 3 ? React.createElement('div', { className: 'uh-card-sub' }, language === 'en' ? 'See the details table for the other ' + (rows.length - 3) + ' workspaces' : '其余 ' + (rows.length - 3) + ' 个工作区见明细表') : null,
691
+ ),
692
+ )
693
+
694
+ const rowElements = rows.map((w) => {
695
+ const meta = wsById.get(w.workspaceId)
696
+ const alias = typeof aliases[w.workspaceId] === 'string' ? aliases[w.workspaceId] : ''
697
+ const folderTitle = meta ? meta.title : tr('未知工作区', 'Unknown workspace')
698
+ const path = meta ? meta.path : ''
699
+ const title = alias !== '' ? alias : folderTitle
700
+ const subText = alias !== '' ? folderTitle + ' · ' + path : path
701
+ const total = wsTotal(w)
702
+ const rate = rateOf(w.input, w.cacheRead)
703
+ const idx = wsIndex.get(w.workspaceId)
704
+ const color = wsColor(idx === undefined ? 0 : idx)
705
+ const selected = wsFilter === w.workspaceId
706
+ return React.createElement('div', {
707
+ key: w.workspaceId,
708
+ className: 'uh-row' + (selected ? ' uh-sel' : ''),
709
+ onClick: () => toggleFilter(w.workspaceId),
710
+ },
711
+ React.createElement('div', {},
712
+ React.createElement('div', { className: 'uh-ws-title' }, title),
713
+ React.createElement('div', { className: 'uh-ws-path' }, subText),
714
+ ),
715
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(w.turns)),
716
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(w.input)),
717
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(w.cacheRead)),
718
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(w.output)),
719
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(w.reasoning)),
720
+ React.createElement('div', {},
721
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(total)),
722
+ React.createElement('div', { className: 'uh-barwrap' },
723
+ React.createElement('div', { className: 'uh-barfill', style: { width: maxTotal > 0 ? Math.max(2, (total / maxTotal) * 100) + '%' : '0%', background: color } }),
724
+ ),
725
+ ),
726
+ React.createElement('div', { className: 'uh-num' }, rate.toFixed(1) + '%'),
727
+ React.createElement('div', { className: 'uh-num' }, maxTotal > 0 ? ((total / maxTotal) * 100).toFixed(0) + '%' : '0%'),
728
+ )
729
+ })
730
+
731
+ const modelViewLabel = modelView === 'route' ? tr('混合查看', 'Combined View') : modelView === 'model' ? tr('按模型合并', 'Grouped by Model') : tr('按供应商汇总', 'Grouped by Provider')
732
+ const modelColumnLabel = modelView === 'route' ? tr('供应商 / 模型', 'Provider / Model') : modelView === 'model' ? tr('模型', 'Model') : tr('供应商', 'Provider')
733
+ const modelRows = aggregateModelRows(agg.perModel || [], modelView, tr('未知供应商', 'Unknown provider'), tr('未知模型', 'Unknown model')).sort((a, b) => wsTotal(b) - wsTotal(a))
734
+ const exportCsv = () => {
735
+ const quote = (value) => '"' + String(value === undefined || value === null ? '' : value).replace(/"/g, '""') + '"'
736
+ const line = (values) => values.map(quote).join(',')
737
+ const allTokens = (entry) => entry.input + entry.output + entry.cacheRead + entry.cacheWrite + entry.reasoning
738
+ const tokenHeaders = [tr('输入 Token', 'Input Tokens'), tr('缓存命中 Token', 'Cache-Hit Tokens'), tr('缓存写入 Token', 'Cache-Write Tokens'), tr('输出 Token', 'Output Tokens'), tr('推理 Token', 'Reasoning Tokens'), tr('总处理 Token', 'Total Tokens Processed'), tr('缓存命中率', 'Cache Hit Rate')]
739
+ const output = [
740
+ line([tr('DSH 用量统计导出', 'DSH Usage Statistics Export')]),
741
+ line([tr('导出时间', 'Exported At'), useUtc ? new Date().toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' }) : new Date().toLocaleString('zh-CN')]),
742
+ line([tr('时间范围', 'Time Range'), rangeLabel]),
743
+ line([tr('模型查看模式', 'Model View Mode'), modelViewLabel]),
744
+ '',
745
+ line([tr('汇总', 'Summary')]),
746
+ line([tr('回合', 'Turns'), tr('会话', 'Sessions'), ...tokenHeaders]),
747
+ line([agg.totals.turns, agg.totals.sessions, agg.totals.input, agg.totals.cacheRead, agg.totals.cacheWrite, agg.totals.output, agg.totals.reasoning, allTokens(agg.totals), rateOf(agg.totals.input, agg.totals.cacheRead).toFixed(2) + '%']),
748
+ '',
749
+ line([tr('模型用量明细', 'Model Usage Details')]),
750
+ line([modelColumnLabel, tr('调用', 'Calls'), ...tokenHeaders]),
751
+ ...modelRows.map((m) => line([m.model, m.calls, m.input, m.cacheRead, m.cacheWrite, m.output, m.reasoning, allTokens(m), rateOf(m.input, m.cacheRead).toFixed(2) + '%'])),
752
+ '',
753
+ line([tr('工作区明细', 'Workspace Details')]),
754
+ line([tr('工作区', 'Workspace'), tr('路径', 'Path'), tr('回合', 'Turns'), ...tokenHeaders]),
755
+ ...rows.map((w) => { const meta = wsById.get(w.workspaceId); return line([wsTitle(w.workspaceId), meta ? meta.path : '', w.turns, w.input, w.cacheRead, w.cacheWrite, w.output, w.reasoning, allTokens(w), rateOf(w.input, w.cacheRead).toFixed(2) + '%']) }),
756
+ ]
757
+ const blob = new Blob(['\uFEFF' + output.join('\r\n')], { type: 'text/csv;charset=utf-8' })
758
+ const url = URL.createObjectURL(blob)
759
+ const anchor = document.createElement('a')
760
+ anchor.href = url
761
+ anchor.download = 'dsh-all-usage-' + range + '-' + modelView + '-' + fmtDate(new Date(), useUtc) + '.csv'
762
+ document.body.appendChild(anchor); anchor.click(); anchor.remove()
763
+ URL.revokeObjectURL(url)
764
+ }
765
+
766
+ const modelElements = modelRows.map((m) => {
767
+ const total = wsTotal(m)
768
+ const rate = rateOf(m.input, m.cacheRead)
769
+ return React.createElement('div', { key: m.model, className: 'uh-model-row uh-row' },
770
+ React.createElement('div', { className: 'uh-ws-title', title: m.model }, m.model),
771
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(m.calls)),
772
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(m.input)),
773
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(m.cacheRead)),
774
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(m.output)),
775
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(m.reasoning)),
776
+ React.createElement('div', { className: 'uh-num' }, fmtCompact(total)),
777
+ React.createElement('div', { className: 'uh-num' }, rate.toFixed(1) + '%'),
778
+ )
779
+ })
780
+
781
+ const aliasPanel = aliasOpen
782
+ ? React.createElement('div', { className: 'uh-panel uh-anim-panel' },
783
+ React.createElement('div', { className: 'uh-alias-panel-head' },
784
+ React.createElement('span', {}, tr('工作区别名', 'Workspace Aliases')),
785
+ React.createElement('button', { className: 'uh-alias-close', onClick: () => setAliasOpen(false) }, tr('关闭', 'Close')),
786
+ ),
787
+ workspaces.length === 0
788
+ ? React.createElement('div', { className: 'uh-empty', style: { padding: '10px 0' } }, tr('暂无工作区', 'No workspaces yet'))
789
+ : React.createElement('div', { className: 'uh-alias-list' },
790
+ workspaces.map((w, i) => React.createElement('div', { key: w.id, className: 'uh-alias-item' },
791
+ React.createElement('span', { className: 'uh-dot', style: { background: wsColor(i) } }),
792
+ React.createElement('span', { className: 'uh-alias-folder', title: w.path }, w.title || w.path),
793
+ React.createElement('input', {
794
+ className: 'uh-alias-input',
795
+ value: aliasDrafts[w.id] !== undefined ? aliasDrafts[w.id] : '',
796
+ placeholder: tr('项目别名', 'Project alias'),
797
+ onChange: (e) => setAliasDrafts((prev) => Object.assign({}, prev, { [w.id]: e.target.value })),
798
+ onKeyDown: (e) => { if (e.key === 'Enter') saveAlias(w.id, e.target.value) },
799
+ }),
800
+ )),
801
+ ),
802
+ React.createElement('div', { className: 'uh-alias-panel-foot' },
803
+ React.createElement('span', { className: 'uh-note' }, tr('回车保存单个;清空别名还原文件夹名', 'Press Enter to save one; clear an alias to restore the folder name')),
804
+ React.createElement('button', { className: 'uh-alias-ok', onClick: saveAllAliases }, tr('全部保存', 'Save All')),
805
+ ),
806
+ )
807
+ : null
808
+
809
+ let tip = null
810
+ if (hover !== null && hover !== undefined) {
811
+ const day = hover.day
812
+ let rowsContent = []
813
+ let tokensText = ''
814
+ if (day !== undefined) {
815
+ const sorted = day.perWorkspace.slice().sort((a, b) => b.turns - a.turns)
816
+ rowsContent = sorted.map((entry) => {
817
+ const idx = wsIndex.get(entry.workspaceId)
818
+ return React.createElement('div', {
819
+ key: entry.workspaceId,
820
+ className: 'uh-tip-row',
821
+ onClick: () => { toggleFilter(entry.workspaceId); setHover(null) },
822
+ },
823
+ React.createElement('span', { className: 'uh-dot', style: { background: wsColor(idx === undefined ? 0 : idx) } }),
824
+ React.createElement('span', {}, wsTitle(entry.workspaceId)),
825
+ React.createElement('span', { className: 'uh-n' }, language === 'en' ? entry.turns + ' uses' : entry.turns + ' 次'),
826
+ )
827
+ })
828
+ if (day.tokens.input + day.tokens.output + day.tokens.cacheRead > 0) {
829
+ tokensText = language === 'en' ? 'Tokens: Input ' + fmtCompact(day.tokens.input) + ' · Cache hits ' + fmtCompact(day.tokens.cacheRead) + ' · Output ' + fmtCompact(day.tokens.output) : 'Token:输入 ' + fmtCompact(day.tokens.input) + ' · 缓存命中 ' + fmtCompact(day.tokens.cacheRead) + ' · 输出 ' + fmtCompact(day.tokens.output)
830
+ }
831
+ }
832
+ const flip = hover.x > 640
833
+ tip = React.createElement('div', {
834
+ key: hover.date,
835
+ className: 'uh-tip',
836
+ style: {
837
+ left: hover.x + 14,
838
+ top: hover.y + 12,
839
+ transform: flip ? 'translateX(calc(-100% - 28px))' : 'none',
840
+ },
841
+ },
842
+ React.createElement('div', { className: 'uh-tip-date' }, humanDate(hover.date, language)),
843
+ day !== undefined && day.turns > 0
844
+ ? rowsContent
845
+ : React.createElement('div', { className: 'uh-empty', style: { padding: '6px 0' } }, tr('这一天没有使用记录', 'No usage records for this day')),
846
+ tokensText !== '' ? React.createElement('div', { className: 'uh-tip-tokens' }, tokensText) : null,
847
+ )
848
+ }
849
+
850
+ const rangeLabel = range === 'today' ? tr('今日', 'Today') : range === '30d' ? tr('近 30 天', 'Last 30 Days') : range === '90d' ? tr('近 90 天', 'Last 90 Days') : tr('全部', 'All Time')
851
+ const scanning = !scan.done
852
+ const pct = scan.total > 0 ? Math.min(100, Math.round((scan.scanned / scan.total) * 100)) : 40
853
+ const isEmpty = scan.done && dayRows.length === 0 && stats.totals.turns === 0
854
+
855
+ return React.createElement('div', { className: 'uh-page' },
856
+ React.createElement('div', { className: 'uh-head' },
857
+ React.createElement('div', { className: 'uh-title-wrap' },
858
+ React.createElement('h2', { className: 'uh-title' }, tr('用量统计', 'Usage Statistics')),
859
+ ),
860
+ React.createElement('div', { className: 'uh-actions' },
861
+ React.createElement('button', {
862
+ className: 'uh-refresh',
863
+ title: tr('管理工作区别名', 'Manage workspace aliases'),
864
+ onClick: () => { if (aliasOpen) setAliasOpen(false); else openAliasPanel() },
865
+ }, React.createElement(LineIcon, { name: 'edit', size: 14 }), tr('工作区别名', 'Workspace Aliases')),
866
+ React.createElement('div', {
867
+ className: 'uh-language-menu',
868
+ ref: languageMenuRef,
869
+ onKeyDown: (event) => { if (event.key === 'Escape') { event.preventDefault(); setLanguageMenuOpen(false) } },
870
+ },
871
+ React.createElement('button', {
872
+ type: 'button',
873
+ className: 'uh-language-trigger' + (languageMenuOpen ? ' uh-open' : ''),
874
+ title: tr('切换界面语言', 'Change interface language'),
875
+ 'aria-label': tr('界面语言', 'Interface language'),
876
+ 'aria-haspopup': 'menu',
877
+ 'aria-expanded': languageMenuOpen,
878
+ onClick: () => setLanguageMenuOpen((open) => !open),
879
+ },
880
+ React.createElement(LineIcon, { name: 'language', size: 14 }),
881
+ React.createElement('span', { className: 'uh-language-label' }, language === 'en' ? 'English' : '中文'),
882
+ React.createElement(LineIcon, { name: 'chevron', size: 13, className: 'uh-language-caret' }),
883
+ ),
884
+ languageMenuOpen ? React.createElement('div', { className: 'uh-language-options', role: 'menu', 'aria-label': tr('界面语言', 'Interface language') },
885
+ [['zh', '中文'], ['en', 'English']].map((entry) => React.createElement('button', {
886
+ key: entry[0],
887
+ type: 'button',
888
+ role: 'menuitemradio',
889
+ 'aria-checked': language === entry[0],
890
+ className: 'uh-language-option' + (language === entry[0] ? ' uh-on' : ''),
891
+ onClick: () => chooseLanguage(entry[0]),
892
+ },
893
+ React.createElement(LineIcon, { name: 'language', size: 14 }),
894
+ React.createElement('span', {}, entry[1]),
895
+ language === entry[0] ? React.createElement(LineIcon, { name: 'check', size: 14, className: 'uh-language-option-check' }) : null,
896
+ )),
897
+ ) : null,
898
+ ),
899
+ React.createElement('div', { className: 'uh-range' },
900
+ ['today', '30d', '90d', 'all'].map((r) => React.createElement('button', {
901
+ key: r,
902
+ className: range === r ? 'uh-on' : '',
903
+ onClick: () => setRange(r),
904
+ }, r === 'today' ? tr('今日', 'Today') : r === '30d' ? tr('近 30 天', 'Last 30 Days') : r === '90d' ? tr('近 90 天', 'Last 90 Days') : tr('全部', 'All Time'))),
905
+ ),
906
+ React.createElement('button', { className: 'uh-refresh', title: tr('导出当前时间范围与模型查看模式的 CSV 数据', 'Export CSV data for the current time range and model view'), onClick: exportCsv }, React.createElement(LineIcon, { name: 'export', size: 14 }), tr('导出数据', 'Export Data')),
907
+ React.createElement('button', { className: 'uh-refresh uh-icon-button', title: tr('刷新统计数据', 'Refresh usage statistics'), 'aria-label': tr('刷新统计数据', 'Refresh usage statistics'), onClick: onRefresh }, React.createElement(LineIcon, { name: 'refresh', size: 16 })),
908
+ ),
909
+ ),
910
+ aliasOpen ? aliasPanel : null,
911
+ scanning ? React.createElement('div', { className: 'uh-progress' },
912
+ React.createElement('span', {}, language === 'en' ? 'Scanning historical sessions: ' + scan.scanned + ' / ' + scan.total + (scan.failed > 0 ? ' (' + scan.failed + ' failed to read)' : '') : '正在统计历史会话 ' + scan.scanned + ' / ' + scan.total + (scan.failed > 0 ? '(' + scan.failed + ' 个读取失败)' : '')),
913
+ React.createElement('div', { className: 'uh-bar' }, React.createElement('div', { className: 'uh-fill', style: { width: pct + '%' } })),
914
+ ) : null,
915
+ isEmpty ? React.createElement('div', { className: 'uh-panel' },
916
+ React.createElement('div', { className: 'uh-empty' }, tr('还没有使用记录。开始对话后,这里会点亮。', 'No usage recorded yet. This area will light up after you start a conversation.')),
917
+ ) : React.createElement(React.Fragment, null,
918
+ React.createElement(React.Fragment, null,
919
+ React.createElement('div', { className: 'uh-ios-summary' },
920
+ React.createElement('div', { className: 'uh-ios-summary-total' },
921
+ React.createElement('div', { className: 'uh-ios-summary-label' }, React.createElement(LineIcon, { name: 'chart', size: 15 }), tr('总处理 Token', 'Total Tokens Processed')),
922
+ React.createElement('div', { className: 'uh-ios-summary-value' }, valueWithMagnitude(fmtCompact(animatedTotal), agg.totals.input + agg.totals.output + agg.totals.cacheRead + agg.totals.cacheWrite + agg.totals.reasoning, language)),
923
+ React.createElement('div', { className: 'uh-ios-summary-caption' }, language === 'en' ? rangeLabel + ' · ' + fmtCompact(animatedTurns) + ' uses · includes cache reads/writes and reasoning' : rangeLabel + ' · ' + fmtCompact(animatedTurns) + ' 次使用 · 含缓存读写与推理'),
924
+ ),
925
+ React.createElement('div', { className: 'uh-ios-summary-cache' },
926
+ React.createElement('div', { className: 'uh-ios-summary-label' }, React.createElement(LineIcon, { name: 'cache', size: 15 }), tr('缓存命中', 'Cache Hits')),
927
+ React.createElement('div', { className: 'uh-ios-summary-value' }, (animatedRate / 10).toFixed(1) + '%'),
928
+ React.createElement('div', { className: 'uh-ios-summary-caption' }, language === 'en' ? fmtCompact(agg.totals.cacheRead) + ' context tokens reused' : '复用上下文 ' + fmtCompact(agg.totals.cacheRead) + ' Token'),
929
+ ),
930
+ ),
931
+ React.createElement('div', { className: 'uh-token-semantics' },
932
+ React.createElement(LineIcon, { name: 'cache', size: 16 }),
933
+ tr('总处理 Token = 输入 + 输出 + 缓存读写 + 推理。缓存命中代表复用上下文,不等于新生成 Token 或实际费用。', 'Total tokens processed = input + output + cache reads/writes + reasoning. Cache hits represent reused context; they are not newly generated tokens or actual cost.'),
934
+ ),
935
+ React.createElement('div', { className: 'uh-cards' },
936
+ card(tr('DeepSeek 账户余额', 'DeepSeek Account Balance'), balanceValue, balanceSub, 0, 'wallet'),
937
+ card(tr('总使用次数', 'Total Uses'), fmtCompact(animatedTurns), range === 'all' ? (language === 'en' ? agg.totals.sessions + ' sessions' : agg.totals.sessions + ' 个会话') : (language === 'en' ? 'Turns in ' + rangeLabel : rangeLabel + '内的回合数'), 4, 'chart'),
938
+ card(tr('连续使用', 'Current Streak'), language === 'en' ? st.streak + ' days' : st.streak + ' 天', language === 'en' ? 'Longest streak: ' + st.best + ' days' : '最长连续 ' + st.best + ' 天', 5, 'clock'),
939
+ tokenCard,
940
+ ),
941
+ React.createElement('div', { className: 'uh-panel' },
942
+ React.createElement('div', { className: 'uh-section-title' }, React.createElement(LineIcon, { name: 'calendar', size: 16 }), tr('使用热力图', 'Usage Heatmap')),
943
+ React.createElement('div', { className: 'uh-hm-head' },
944
+ React.createElement('div', { className: 'uh-chips' }, workspaces.map((w, i) => {
945
+ const on = wsFilter === w.id
946
+ return React.createElement('button', {
947
+ key: w.id,
948
+ className: 'uh-chip' + (on ? ' uh-on' : ''),
949
+ onClick: () => toggleFilter(w.id),
950
+ title: w.path,
951
+ },
952
+ React.createElement('span', { className: 'uh-dot', style: { background: wsColor(i) } }),
953
+ React.createElement('span', { className: 'uh-chip-title' }, wsTitle(w.id)),
954
+ )
955
+ })),
956
+ React.createElement('div', { className: 'uh-legend' },
957
+ React.createElement('span', {}, tr('少', 'Less')),
958
+ [0, 1, 2, 3, 4].map((l) => React.createElement('span', { key: l, className: 'uh-cell', style: { background: cellBg(l) } })),
959
+ React.createElement('span', {}, tr('多', 'More')),
960
+ ),
961
+ ),
962
+ React.createElement('div', { className: 'uh-hm-scroll' },
963
+ React.createElement('div', { className: 'uh-months' }, monthLabels.map((m, i) => React.createElement('span', { key: i, style: { left: m.left } }, m.text))),
964
+ React.createElement('div', { className: 'uh-hm-body' },
965
+ React.createElement('div', { className: 'uh-wdays' }, weekdayLabels.map((w, i) => React.createElement('span', { key: i }, w))),
966
+ React.createElement('div', { className: 'uh-grid' }, cellElements),
967
+ ),
968
+ ),
969
+ React.createElement('div', { className: 'uh-note', style: { marginTop: 10 } }, tr('口径:每完成一个回合点亮一次(含子代理会话);悬停查看按工作区明细,点击工作区可筛选热力图与明细表。日期按本地时区。', 'Methodology: one cell lights up for each completed turn, including subagent sessions. Hover to view workspace details; click a workspace to filter the heatmap and detail tables. English dates and day boundaries use UTC.')),
970
+ ),
971
+ ),
972
+ React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
973
+ React.createElement('div', { className: 'uh-hm-head' },
974
+ React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon', style: { margin: 0 } }, React.createElement(LineIcon, { name: 'chart', size: 16 }), language === 'en' ? 'Model Usage Details (' + rangeLabel + ')' : '模型用量明细(' + rangeLabel + ')'),
975
+ React.createElement('div', { className: 'uh-range' },
976
+ [['route', tr('混合查看', 'Combined View')], ['model', tr('按模型', 'By Model')], ['provider', tr('按供应商', 'By Provider')]].map((entry) => React.createElement('button', {
977
+ key: entry[0], className: modelView === entry[0] ? 'uh-on' : '', onClick: () => setModelView(entry[0]),
978
+ }, entry[1])),
979
+ ),
980
+ ),
981
+ modelRows.length === 0
982
+ ? React.createElement('div', { className: 'uh-empty' }, tr('尚无带模型路由信息的用量记录', 'No usage records with model-routing information yet'))
983
+ : React.createElement('div', { className: 'uh-tbl-scroll' },
984
+ React.createElement('div', { className: 'uh-model-hrow uh-hrow' },
985
+ React.createElement('div', {}, modelColumnLabel),
986
+ React.createElement('div', { className: 'uh-num' }, tr('调用', 'Calls')),
987
+ React.createElement('div', { className: 'uh-num' }, tr('输入', 'Input')),
988
+ React.createElement('div', { className: 'uh-num' }, tr('缓存命中', 'Cache Hits')),
989
+ React.createElement('div', { className: 'uh-num' }, tr('输出', 'Output')),
990
+ React.createElement('div', { className: 'uh-num' }, tr('推理', 'Reasoning')),
991
+ React.createElement('div', { className: 'uh-num' }, tr('总处理', 'Total Processed')),
992
+ React.createElement('div', { className: 'uh-num' }, tr('命中率', 'Hit Rate')),
993
+ ),
994
+ modelElements,
995
+ ),
996
+ React.createElement('div', { className: 'uh-note', style: { marginTop: 10 } }, language === 'en' ? modelViewLabel + ': Combined View distinguishes “Provider / Model”; By Model merges identically named models across providers; By Provider aggregates all of a provider’s models. Historical records without routing information are grouped as “Unknown.”' : modelViewLabel + ':混合查看按“供应商 / 模型”区分;按模型会跨供应商合并同名模型;按供应商则汇总其全部模型。缺少路由信息的历史记录会归为“未知”。'),
997
+ ),
998
+ React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
999
+ React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon' }, React.createElement(LineIcon, { name: 'folder', size: 16 }), language === 'en' ? 'Workspace Details (' + rangeLabel + ')' : '工作区明细(' + rangeLabel + ')'),
1000
+ rows.length === 0
1001
+ ? React.createElement('div', { className: 'uh-empty' }, tr('该时间范围内没有使用记录', 'No usage records in this time range'))
1002
+ : React.createElement('div', { className: 'uh-tbl-scroll' },
1003
+ React.createElement('div', { className: 'uh-hrow' },
1004
+ React.createElement('div', {}, tr('工作区', 'Workspace')),
1005
+ React.createElement('div', { className: 'uh-num' }, tr('回合', 'Turns')),
1006
+ React.createElement('div', { className: 'uh-num' }, tr('输入', 'Input')),
1007
+ React.createElement('div', { className: 'uh-num' }, tr('缓存命中', 'Cache Hits')),
1008
+ React.createElement('div', { className: 'uh-num' }, tr('输出', 'Output')),
1009
+ React.createElement('div', { className: 'uh-num' }, tr('推理', 'Reasoning')),
1010
+ React.createElement('div', { className: 'uh-num' }, tr('总处理', 'Total Processed')),
1011
+ React.createElement('div', { className: 'uh-num' }, tr('命中率', 'Hit Rate')),
1012
+ React.createElement('div', { className: 'uh-num' }, tr('占比', 'Share')),
1013
+ ),
1014
+ rowElements,
1015
+ ),
1016
+ ),
1017
+ ),
1018
+ tip,
1019
+ )
1020
+ }
1021
+
1022
+ function UsageSidebarEntry(props) {
1023
+ const [open, setOpen] = React.useState(false)
1024
+ const [language, setLanguage] = React.useState(storedLanguage)
1025
+ const tr = (zh, en) => language === 'en' ? en : zh
1026
+ const changeLanguage = (next) => {
1027
+ const value = next === 'en' ? 'en' : 'zh'
1028
+ setLanguage(value)
1029
+ persistLanguage(value)
1030
+ }
1031
+ React.useEffect(() => {
1032
+ if (!open) return undefined
1033
+ const closeOnEscape = (event) => { if (event.key === 'Escape') setOpen(false) }
1034
+ document.addEventListener('keydown', closeOnEscape)
1035
+ return () => document.removeEventListener('keydown', closeOnEscape)
1036
+ }, [open])
1037
+ return React.createElement(React.Fragment, null,
1038
+ React.createElement('button', {
1039
+ type: 'button', className: 'uh-side-entry', title: tr('用量统计', 'Usage Statistics'), 'aria-label': tr('用量统计', 'Usage Statistics'), onClick: () => setOpen(true),
1040
+ }, React.createElement('span', { className: 'uh-side-entry-icon' }, React.createElement(LineIcon, { name: 'chart', size: 17 })), props.wide ? React.createElement('span', { className: 'uh-side-entry-label' }, tr('用量统计', 'Usage Statistics')) : null),
1041
+ open ? React.createElement('div', { className: 'uh-side-modal', role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) setOpen(false) } },
1042
+ React.createElement('div', { className: 'uh-side-dialog', role: 'dialog', 'aria-modal': true, 'aria-label': tr('用量统计', 'Usage Statistics') },
1043
+ React.createElement('div', { className: 'uh-side-dialog-head' },
1044
+ React.createElement('button', { className: 'uh-refresh uh-close-button', type: 'button', title: tr('关闭用量统计', 'Close Usage Statistics'), 'aria-label': tr('关闭用量统计', 'Close Usage Statistics'), onClick: () => setOpen(false) }, React.createElement(LineIcon, { name: 'close', size: 18 })),
1045
+ ),
1046
+ React.createElement(UsagePage, { timerCtx: props.timerCtx, language, onLanguageChange: changeLanguage }),
1047
+ ),
1048
+ ) : null,
1049
+ )
1050
+ }
1051
+
1052
+ exports.inject = ['timer', 'slots']
1053
+ exports.apply = (ctx) => {
1054
+ const slots = ctx.get('slots')
1055
+ if (slots === undefined) return
1056
+ slots.inject('sidebar.footer.action', () => slots.register(
1057
+ { name: 'sidebar.footer.action', id: 'all-usage', order: 10 },
1058
+ (props) => React.createElement(UsageSidebarEntry, { wide: props.wide, timerCtx: ctx }),
1059
+ ))
1060
+ }
1061
+ return module.exports;
1062
+ }
1063
+ });