dsh-model-usage 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/CHANGELOG.md +57 -45
- package/LICENSE +21 -21
- package/README.md +312 -295
- package/cordis.patch.yml +11 -11
- package/docs/screenshots/u1.png +0 -0
- package/docs/screenshots/u2.png +0 -0
- package/docs/screenshots/u3.png +0 -0
- package/docs/screenshots/u4.png +0 -0
- package/docs/screenshots/u5.png +0 -0
- package/icon.svg +29 -7
- package/lib/client.js +1787 -1410
- package/lib/index.js +1083 -1083
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +84 -84
- package/docs/screenshots/usage-panel.png +0 -0
package/lib/client.js
CHANGED
|
@@ -1,1410 +1,1787 @@
|
|
|
1
|
-
// Browser half of dsh-model-usage — a hand-built client bundle in the
|
|
2
|
-
// closure-factory format client-modules expects. It registers one entry into
|
|
3
|
-
// the session header's right-aligned utilities slot ('用量') and renders a
|
|
4
|
-
// modal with the usage of the model THIS CONVERSATION is running on: the model
|
|
5
|
-
// comes from the Session's `modelSelection` projection (the same fact the model
|
|
6
|
-
// picker shows; the wire view is `{ lastUsed, next }`), the key comes from that
|
|
7
|
-
// provider's own credential reference on the host, and the numbers come from
|
|
8
|
-
// the matching usage API. The host resolves and holds every secret; this half
|
|
9
|
-
// only ever sees the normalized payload plus the credential REFERENCE NAME.
|
|
10
|
-
//
|
|
11
|
-
// Everything shared with the host is carried by ids and tokens only:
|
|
12
|
-
// - React comes from the loader's module table, and nothing else is imported:
|
|
13
|
-
// the controls below are this plugin's own, styled exclusively with the
|
|
14
|
-
// theme tokens the running build lists.
|
|
15
|
-
// - Copy lives in the Client locale service (`ctx.locale`), so the panel
|
|
16
|
-
// follows the active language and switches live.
|
|
17
|
-
// - The stylesheet renders as a React element inside the cell, so it appears
|
|
18
|
-
// and disappears with this slot entry instead of leaking a <style> tag.
|
|
19
|
-
//
|
|
20
|
-
// Refreshing is a background concern, not a click handler: this cell mounts
|
|
21
|
-
// with the session view, so it queries once on mount (and on every model
|
|
22
|
-
// switch) and then keeps the numbers warm on its own timer while 自动刷新 is on.
|
|
23
|
-
// Interval and toggle live in localStorage (`dsh-model-usage:refresh`).
|
|
24
|
-
window.__ModuleLoader__.load({
|
|
25
|
-
id: "dsh-model-usage",
|
|
26
|
-
factory: (require) => {
|
|
27
|
-
var module = { exports: {} };
|
|
28
|
-
var exports = module.exports;
|
|
29
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
30
|
-
|
|
31
|
-
const React = require("react");
|
|
32
|
-
|
|
33
|
-
/** Locale namespace every user-visible string of this plugin belongs to. */
|
|
34
|
-
const NS = "dsh-model-usage";
|
|
35
|
-
// Host routes are document-relative (like every shipped client plugin's own
|
|
36
|
-
// routes) so the panel also works when the GUI is served under a directory
|
|
37
|
-
// mount instead of the site root.
|
|
38
|
-
const ROUTE_BALANCE = "model-usage/balance";
|
|
39
|
-
const ROUTE_PROVIDERS = "model-usage/providers";
|
|
40
|
-
const ROUTE_MODELS = "model-usage/models";
|
|
41
|
-
|
|
42
|
-
// -----------------------------------------------------------------------
|
|
43
|
-
// Copy. Keys are shared by both dictionaries; `t` reads the active locale
|
|
44
|
-
// at call time and the slot system re-renders this cell on a switch.
|
|
45
|
-
// Provider errors and endpoint field names stay as the host reports them —
|
|
46
|
-
// they are data about a third-party service, not interface copy.
|
|
47
|
-
const zh = {
|
|
48
|
-
"button.label": "用量",
|
|
49
|
-
"button.titleAuto": "用量(每 {interval}自动刷新)",
|
|
50
|
-
"button.titleManual": "用量(自动刷新已关闭)",
|
|
51
|
-
"action.refresh": "刷新",
|
|
52
|
-
"action.retry": "重试",
|
|
53
|
-
"action.save": "保存",
|
|
54
|
-
"action.close": "关闭",
|
|
55
|
-
"action.cancel": "取消",
|
|
56
|
-
"action.info": "说明",
|
|
57
|
-
"action.auto": "改回自动",
|
|
58
|
-
"age.now": "刚刚",
|
|
59
|
-
"age.minutes": "{count} 分钟前",
|
|
60
|
-
"age.hours": "{count} 小时前",
|
|
61
|
-
"age.days": "{count} 天前",
|
|
62
|
-
"minutes.value": "{count} 分钟",
|
|
63
|
-
"status.refreshing": "刷新中…",
|
|
64
|
-
"status.refreshingAge": "刷新中… · 当前显示{age}数据",
|
|
65
|
-
"status.never": "尚未刷新",
|
|
66
|
-
"status.updated": "刷新时间:{age}",
|
|
67
|
-
"status.stale": " · 已超过刷新间隔({interval})",
|
|
68
|
-
"status.autoOn": "自动刷新:开",
|
|
69
|
-
"status.autoOff": "自动刷新:关",
|
|
70
|
-
"status.autoTooltipOn": "每 {interval}在后台刷新(关闭弹窗也生效)",
|
|
71
|
-
"status.autoTooltipOff": "后台自动刷新已关闭,仅在打开弹窗且数据过期时查询",
|
|
72
|
-
"status.settings": "刷新设置",
|
|
73
|
-
"state.loading": "读取中…",
|
|
74
|
-
"state.querying": "查询中…",
|
|
75
|
-
"state.empty": "暂无数据",
|
|
76
|
-
"error.lastRefresh": "上次刷新失败:{error}",
|
|
77
|
-
"providers.none": "当前部署没有声明任何模型供应商",
|
|
78
|
-
"providers.summary": "共 {total} 个供应商路由 · {configured} 个密钥已配置 · {supported} 个支持用量查询",
|
|
79
|
-
"providers.keyConfigured": "密钥已配置",
|
|
80
|
-
"providers.keyMissing": "密钥未配置",
|
|
81
|
-
"providers.configuredUnsupported": "密钥已配置 · 不支持用量查询",
|
|
82
|
-
"providers.collapse": "只看已配置 / 支持用量的",
|
|
83
|
-
"providers.expand": "显示其余 {count} 个未配置路由",
|
|
84
|
-
"modelbar.label": "查询对象",
|
|
85
|
-
"modelbar.auto": "自动 · 跟随当前对话",
|
|
86
|
-
"modelbar.autoRoute": "自动 · {route}",
|
|
87
|
-
"modelbar.titleAuto": "当前跟随对话使用的模型,点击可手动切换",
|
|
88
|
-
"modelbar.titleManual": "手动指定的模型,点击可切换或改回自动",
|
|
89
|
-
"picker.title": "选择模型",
|
|
90
|
-
"picker.auto": "自动(跟随当前对话)",
|
|
91
|
-
"picker.default": "部署默认:{route}",
|
|
92
|
-
"picker.empty": "当前没有可路由的模型(检查 设置 → 模型)",
|
|
93
|
-
"picker.hint": "选一个供应商的模型来查询它的用量;这不改变当前对话使用的模型,只改变插件查询的对象。",
|
|
94
|
-
"settings.title": "刷新设置",
|
|
95
|
-
"settings.autoLabel": "后台自动刷新",
|
|
96
|
-
"settings.on": "开启",
|
|
97
|
-
"settings.off": "关闭",
|
|
98
|
-
"settings.autoHint": "开启后,即使用量弹窗没打开,也会按下面的间隔在后台查询并更新;打开弹窗时直接显示最近一次结果。",
|
|
99
|
-
"settings.interval": "刷新间隔",
|
|
100
|
-
"settings.customHint": "也可以直接填写分钟数(1–1440)。",
|
|
101
|
-
"settings.customInvalid": "请填写 1–1440 之间的分钟数。",
|
|
102
|
-
"settings.staleHint": "上次刷新超过刷新间隔时,打开弹窗会先显示上次的用量并标记「刷新中…」,查询完成后自动更新为「刷新时间:刚刚」。",
|
|
103
|
-
"info.title": "用量说明",
|
|
104
|
-
"info.hint": "用量按「当前对话使用的模型」查询:模型来自会话的模型选择,密钥来自该供应商自己的凭据引用(设置 → 模型 里填写),不会使用其它供应商的 key。",
|
|
105
|
-
"card.subscribed": "订阅中",
|
|
106
|
-
"card.windowLimit": "窗口限制",
|
|
107
|
-
"card.notApplicable": "不适用",
|
|
108
|
-
"card.credits": "信用余额",
|
|
109
|
-
"card.monthlyRemaining": "月度剩余",
|
|
110
|
-
"card.spent": "已花费",
|
|
111
|
-
"card.requests": "请求数",
|
|
112
|
-
"card.account": "账户",
|
|
113
|
-
"card.cycleEnd": "本期截止",
|
|
114
|
-
"card.balance": "余额",
|
|
115
|
-
"card.balanceEmpty": "接口未返回 balance_infos",
|
|
116
|
-
"card.available": "账户可用",
|
|
117
|
-
"card.unavailable": "账户不可用",
|
|
118
|
-
"card.deepseekAccount": "DeepSeek 账户",
|
|
119
|
-
"card.noWallet": "账户没有可用钱包",
|
|
120
|
-
"card.windowFallback": "窗口",
|
|
121
|
-
"card.raw": "原始返回",
|
|
122
|
-
"card.hideRaw": "隐藏原始返回",
|
|
123
|
-
"card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"card.
|
|
224
|
-
"card.
|
|
225
|
-
"card.
|
|
226
|
-
"card.
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
".
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
".dsh-usage-
|
|
315
|
-
".dsh-usage-
|
|
316
|
-
".dsh-usage-
|
|
317
|
-
".dsh-usage-
|
|
318
|
-
".dsh-usage-
|
|
319
|
-
".dsh-usage-
|
|
320
|
-
".dsh-usage-
|
|
321
|
-
".dsh-usage-
|
|
322
|
-
".dsh-usage-
|
|
323
|
-
".dsh-usage-
|
|
324
|
-
".dsh-usage-
|
|
325
|
-
".dsh-usage-
|
|
326
|
-
".dsh-usage-
|
|
327
|
-
".dsh-usage-
|
|
328
|
-
".dsh-usage-
|
|
329
|
-
".dsh-usage-
|
|
330
|
-
".dsh-usage-
|
|
331
|
-
".dsh-usage-
|
|
332
|
-
".dsh-usage-
|
|
333
|
-
".dsh-usage-
|
|
334
|
-
".dsh-usage-
|
|
335
|
-
".dsh-usage-
|
|
336
|
-
".dsh-usage-
|
|
337
|
-
".dsh-usage-
|
|
338
|
-
".dsh-usage-
|
|
339
|
-
".dsh-usage-
|
|
340
|
-
".dsh-usage-
|
|
341
|
-
".dsh-usage-
|
|
342
|
-
".dsh-usage-
|
|
343
|
-
".dsh-usage-
|
|
344
|
-
".dsh-usage-
|
|
345
|
-
".dsh-usage-
|
|
346
|
-
".dsh-usage-
|
|
347
|
-
".dsh-usage-
|
|
348
|
-
".dsh-usage-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
return
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
} else if (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
React.createElement("
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
React.createElement("div", { className: "dsh-usage-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
function
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
React.createElement(
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
React.createElement(
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1
|
+
// Browser half of dsh-model-usage — a hand-built client bundle in the
|
|
2
|
+
// closure-factory format client-modules expects. It registers one entry into
|
|
3
|
+
// the session header's right-aligned utilities slot ('用量') and renders a
|
|
4
|
+
// modal with the usage of the model THIS CONVERSATION is running on: the model
|
|
5
|
+
// comes from the Session's `modelSelection` projection (the same fact the model
|
|
6
|
+
// picker shows; the wire view is `{ lastUsed, next }`), the key comes from that
|
|
7
|
+
// provider's own credential reference on the host, and the numbers come from
|
|
8
|
+
// the matching usage API. The host resolves and holds every secret; this half
|
|
9
|
+
// only ever sees the normalized payload plus the credential REFERENCE NAME.
|
|
10
|
+
//
|
|
11
|
+
// Everything shared with the host is carried by ids and tokens only:
|
|
12
|
+
// - React comes from the loader's module table, and nothing else is imported:
|
|
13
|
+
// the controls below are this plugin's own, styled exclusively with the
|
|
14
|
+
// theme tokens the running build lists.
|
|
15
|
+
// - Copy lives in the Client locale service (`ctx.locale`), so the panel
|
|
16
|
+
// follows the active language and switches live.
|
|
17
|
+
// - The stylesheet renders as a React element inside the cell, so it appears
|
|
18
|
+
// and disappears with this slot entry instead of leaking a <style> tag.
|
|
19
|
+
//
|
|
20
|
+
// Refreshing is a background concern, not a click handler: this cell mounts
|
|
21
|
+
// with the session view, so it queries once on mount (and on every model
|
|
22
|
+
// switch) and then keeps the numbers warm on its own timer while 自动刷新 is on.
|
|
23
|
+
// Interval and toggle live in localStorage (`dsh-model-usage:refresh`).
|
|
24
|
+
window.__ModuleLoader__.load({
|
|
25
|
+
id: "dsh-model-usage",
|
|
26
|
+
factory: (require) => {
|
|
27
|
+
var module = { exports: {} };
|
|
28
|
+
var exports = module.exports;
|
|
29
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
30
|
+
|
|
31
|
+
const React = require("react");
|
|
32
|
+
|
|
33
|
+
/** Locale namespace every user-visible string of this plugin belongs to. */
|
|
34
|
+
const NS = "dsh-model-usage";
|
|
35
|
+
// Host routes are document-relative (like every shipped client plugin's own
|
|
36
|
+
// routes) so the panel also works when the GUI is served under a directory
|
|
37
|
+
// mount instead of the site root.
|
|
38
|
+
const ROUTE_BALANCE = "model-usage/balance";
|
|
39
|
+
const ROUTE_PROVIDERS = "model-usage/providers";
|
|
40
|
+
const ROUTE_MODELS = "model-usage/models";
|
|
41
|
+
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
// Copy. Keys are shared by both dictionaries; `t` reads the active locale
|
|
44
|
+
// at call time and the slot system re-renders this cell on a switch.
|
|
45
|
+
// Provider errors and endpoint field names stay as the host reports them —
|
|
46
|
+
// they are data about a third-party service, not interface copy.
|
|
47
|
+
const zh = {
|
|
48
|
+
"button.label": "用量",
|
|
49
|
+
"button.titleAuto": "用量(每 {interval}自动刷新)",
|
|
50
|
+
"button.titleManual": "用量(自动刷新已关闭)",
|
|
51
|
+
"action.refresh": "刷新",
|
|
52
|
+
"action.retry": "重试",
|
|
53
|
+
"action.save": "保存",
|
|
54
|
+
"action.close": "关闭",
|
|
55
|
+
"action.cancel": "取消",
|
|
56
|
+
"action.info": "说明",
|
|
57
|
+
"action.auto": "改回自动",
|
|
58
|
+
"age.now": "刚刚",
|
|
59
|
+
"age.minutes": "{count} 分钟前",
|
|
60
|
+
"age.hours": "{count} 小时前",
|
|
61
|
+
"age.days": "{count} 天前",
|
|
62
|
+
"minutes.value": "{count} 分钟",
|
|
63
|
+
"status.refreshing": "刷新中…",
|
|
64
|
+
"status.refreshingAge": "刷新中… · 当前显示{age}数据",
|
|
65
|
+
"status.never": "尚未刷新",
|
|
66
|
+
"status.updated": "刷新时间:{age}",
|
|
67
|
+
"status.stale": " · 已超过刷新间隔({interval})",
|
|
68
|
+
"status.autoOn": "自动刷新:开",
|
|
69
|
+
"status.autoOff": "自动刷新:关",
|
|
70
|
+
"status.autoTooltipOn": "每 {interval}在后台刷新(关闭弹窗也生效)",
|
|
71
|
+
"status.autoTooltipOff": "后台自动刷新已关闭,仅在打开弹窗且数据过期时查询",
|
|
72
|
+
"status.settings": "刷新设置",
|
|
73
|
+
"state.loading": "读取中…",
|
|
74
|
+
"state.querying": "查询中…",
|
|
75
|
+
"state.empty": "暂无数据",
|
|
76
|
+
"error.lastRefresh": "上次刷新失败:{error}",
|
|
77
|
+
"providers.none": "当前部署没有声明任何模型供应商",
|
|
78
|
+
"providers.summary": "共 {total} 个供应商路由 · {configured} 个密钥已配置 · {supported} 个支持用量查询",
|
|
79
|
+
"providers.keyConfigured": "密钥已配置",
|
|
80
|
+
"providers.keyMissing": "密钥未配置",
|
|
81
|
+
"providers.configuredUnsupported": "密钥已配置 · 不支持用量查询",
|
|
82
|
+
"providers.collapse": "只看已配置 / 支持用量的",
|
|
83
|
+
"providers.expand": "显示其余 {count} 个未配置路由",
|
|
84
|
+
"modelbar.label": "查询对象",
|
|
85
|
+
"modelbar.auto": "自动 · 跟随当前对话",
|
|
86
|
+
"modelbar.autoRoute": "自动 · {route}",
|
|
87
|
+
"modelbar.titleAuto": "当前跟随对话使用的模型,点击可手动切换",
|
|
88
|
+
"modelbar.titleManual": "手动指定的模型,点击可切换或改回自动",
|
|
89
|
+
"picker.title": "选择模型",
|
|
90
|
+
"picker.auto": "自动(跟随当前对话)",
|
|
91
|
+
"picker.default": "部署默认:{route}",
|
|
92
|
+
"picker.empty": "当前没有可路由的模型(检查 设置 → 模型)",
|
|
93
|
+
"picker.hint": "选一个供应商的模型来查询它的用量;这不改变当前对话使用的模型,只改变插件查询的对象。",
|
|
94
|
+
"settings.title": "刷新设置",
|
|
95
|
+
"settings.autoLabel": "后台自动刷新",
|
|
96
|
+
"settings.on": "开启",
|
|
97
|
+
"settings.off": "关闭",
|
|
98
|
+
"settings.autoHint": "开启后,即使用量弹窗没打开,也会按下面的间隔在后台查询并更新;打开弹窗时直接显示最近一次结果。",
|
|
99
|
+
"settings.interval": "刷新间隔",
|
|
100
|
+
"settings.customHint": "也可以直接填写分钟数(1–1440)。",
|
|
101
|
+
"settings.customInvalid": "请填写 1–1440 之间的分钟数。",
|
|
102
|
+
"settings.staleHint": "上次刷新超过刷新间隔时,打开弹窗会先显示上次的用量并标记「刷新中…」,查询完成后自动更新为「刷新时间:刚刚」。",
|
|
103
|
+
"info.title": "用量说明",
|
|
104
|
+
"info.hint": "用量按「当前对话使用的模型」查询:模型来自会话的模型选择,密钥来自该供应商自己的凭据引用(设置 → 模型 里填写),不会使用其它供应商的 key。",
|
|
105
|
+
"card.subscribed": "订阅中",
|
|
106
|
+
"card.windowLimit": "窗口限制",
|
|
107
|
+
"card.notApplicable": "不适用",
|
|
108
|
+
"card.credits": "信用余额",
|
|
109
|
+
"card.monthlyRemaining": "月度剩余",
|
|
110
|
+
"card.spent": "已花费",
|
|
111
|
+
"card.requests": "请求数",
|
|
112
|
+
"card.account": "账户",
|
|
113
|
+
"card.cycleEnd": "本期截止",
|
|
114
|
+
"card.balance": "余额",
|
|
115
|
+
"card.balanceEmpty": "接口未返回 balance_infos",
|
|
116
|
+
"card.available": "账户可用",
|
|
117
|
+
"card.unavailable": "账户不可用",
|
|
118
|
+
"card.deepseekAccount": "DeepSeek 账户",
|
|
119
|
+
"card.noWallet": "账户没有可用钱包",
|
|
120
|
+
"card.windowFallback": "窗口",
|
|
121
|
+
"card.raw": "原始返回",
|
|
122
|
+
"card.hideRaw": "隐藏原始返回",
|
|
123
|
+
"card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
|
|
124
|
+
"history.title": "余额消耗趋势",
|
|
125
|
+
"history.range.today": "今天",
|
|
126
|
+
"history.range.yesterday": "昨天",
|
|
127
|
+
"history.range.week": "近一周",
|
|
128
|
+
"history.range.month": "近一月",
|
|
129
|
+
"history.range.custom": "自定义",
|
|
130
|
+
"history.start": "开始日期",
|
|
131
|
+
"history.end": "结束日期",
|
|
132
|
+
"history.apply": "应用时间段",
|
|
133
|
+
"history.empty": "该时间段暂无足够的余额记录",
|
|
134
|
+
"history.amount": "消耗",
|
|
135
|
+
"history.clearMonth": "清除所选月份",
|
|
136
|
+
"history.confirmClearMonth": "确定清除 {month} 的余额记录吗?",
|
|
137
|
+
"history.reset": "重置当前记录",
|
|
138
|
+
"history.month": "数据月份",
|
|
139
|
+
"history.noMonths": "暂无可清理的月份",
|
|
140
|
+
"history.confirmReset": "确定清除当前账户的全部余额记录吗?",
|
|
141
|
+
"card.used": "已用",
|
|
142
|
+
"card.total": "总额",
|
|
143
|
+
"window.fiveHour": "5 小时窗口",
|
|
144
|
+
"window.weekly": "7 天窗口",
|
|
145
|
+
"window.usedReset": "已用 {percent}% · 重置还有 {duration}",
|
|
146
|
+
"window.reset": "窗口已重置",
|
|
147
|
+
"duration.reset": "已重置",
|
|
148
|
+
"currency.CNY": "人民币(CNY)",
|
|
149
|
+
"meta.model": "模型",
|
|
150
|
+
"meta.credential": "密钥",
|
|
151
|
+
"meta.accountLogin": "账户登录(无 API Key)",
|
|
152
|
+
"meta.usageUrl": "用量页",
|
|
153
|
+
"kind.commandcode": "Command Code 用量(窗口 / 信用)",
|
|
154
|
+
"kind.deepseek": "DeepSeek 官方余额",
|
|
155
|
+
"kind.deepseek-account": "DeepSeek 账户钱包余额",
|
|
156
|
+
"kind.opencode-go": "OpenCode Go 订阅用量",
|
|
157
|
+
"kind.openrouter": "OpenRouter 额度",
|
|
158
|
+
"kind.moonshot": "Moonshot / Kimi 余额",
|
|
159
|
+
"kind.siliconflow": "SiliconFlow 余额",
|
|
160
|
+
"kind.newapi": "New API 兼容额度",
|
|
161
|
+
"kind.none": "不支持用量查询",
|
|
162
|
+
"state.noHost": "无法连接本机宿主路由:插件可能没有正确挂载(查看 设置 → 插件)",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const en = {
|
|
166
|
+
"button.label": "Usage",
|
|
167
|
+
"button.titleAuto": "Usage (refreshes every {interval})",
|
|
168
|
+
"button.titleManual": "Usage (auto refresh off)",
|
|
169
|
+
"action.refresh": "Refresh",
|
|
170
|
+
"action.retry": "Retry",
|
|
171
|
+
"action.save": "Save",
|
|
172
|
+
"action.close": "Close",
|
|
173
|
+
"action.cancel": "Cancel",
|
|
174
|
+
"action.info": "About",
|
|
175
|
+
"action.auto": "Follow the conversation",
|
|
176
|
+
"age.now": "just now",
|
|
177
|
+
"age.minutes": "{count} min ago",
|
|
178
|
+
"age.hours": "{count} h ago",
|
|
179
|
+
"age.days": "{count} d ago",
|
|
180
|
+
"minutes.value": "{count} min",
|
|
181
|
+
"status.refreshing": "Refreshing…",
|
|
182
|
+
"status.refreshingAge": "Refreshing… · showing data from {age}",
|
|
183
|
+
"status.never": "Not refreshed yet",
|
|
184
|
+
"status.updated": "Updated {age}",
|
|
185
|
+
"status.stale": " · older than the refresh interval ({interval})",
|
|
186
|
+
"status.autoOn": "Auto refresh: on",
|
|
187
|
+
"status.autoOff": "Auto refresh: off",
|
|
188
|
+
"status.autoTooltipOn": "Refreshing in the background every {interval} (works with the panel closed)",
|
|
189
|
+
"status.autoTooltipOff": "Background auto refresh is off; a query runs only when the panel opens with stale numbers",
|
|
190
|
+
"status.settings": "Refresh settings",
|
|
191
|
+
"state.loading": "Loading…",
|
|
192
|
+
"state.querying": "Querying…",
|
|
193
|
+
"state.empty": "No data yet",
|
|
194
|
+
"error.lastRefresh": "Last refresh failed: {error}",
|
|
195
|
+
"providers.none": "This deployment declares no model providers",
|
|
196
|
+
"providers.summary": "{total} provider routes · {configured} with a configured key · {supported} support usage queries",
|
|
197
|
+
"providers.keyConfigured": "Key configured",
|
|
198
|
+
"providers.keyMissing": "Key not configured",
|
|
199
|
+
"providers.configuredUnsupported": "Key configured · usage query not supported",
|
|
200
|
+
"providers.collapse": "Show only configured / supported routes",
|
|
201
|
+
"providers.expand": "Show the other {count} unconfigured routes",
|
|
202
|
+
"modelbar.label": "Target",
|
|
203
|
+
"modelbar.auto": "Auto · follows the conversation",
|
|
204
|
+
"modelbar.autoRoute": "Auto · {route}",
|
|
205
|
+
"modelbar.titleAuto": "Following the conversation's model; click to pick another target",
|
|
206
|
+
"modelbar.titleManual": "A manually picked target; click to change it or go back to auto",
|
|
207
|
+
"picker.title": "Choose a model",
|
|
208
|
+
"picker.auto": "Auto (follow the conversation)",
|
|
209
|
+
"picker.default": "Deployment default: {route}",
|
|
210
|
+
"picker.empty": "No routable model right now (check Settings → Models)",
|
|
211
|
+
"picker.hint": "Pick any provider's model to inspect its usage. This does not change the model this conversation runs on, only what the plugin queries.",
|
|
212
|
+
"settings.title": "Refresh settings",
|
|
213
|
+
"settings.autoLabel": "Background auto refresh",
|
|
214
|
+
"settings.on": "On",
|
|
215
|
+
"settings.off": "Off",
|
|
216
|
+
"settings.autoHint": "While on, the panel keeps querying in the background at the interval below even when it is closed; opening it shows the most recent result right away.",
|
|
217
|
+
"settings.interval": "Refresh interval",
|
|
218
|
+
"settings.customHint": "You can also type a number of minutes (1–1440).",
|
|
219
|
+
"settings.customInvalid": "Enter a number of minutes between 1 and 1440.",
|
|
220
|
+
"settings.staleHint": "When the last refresh is older than the interval, opening the panel first shows the previous numbers marked as refreshing and replaces them once the query returns.",
|
|
221
|
+
"info.title": "Usage details",
|
|
222
|
+
"info.hint": "Usage is queried for the model this conversation runs on: the model comes from the session's model selection, and the key from that provider's own credential reference (set in Settings → Models). No other provider's key is used.",
|
|
223
|
+
"card.subscribed": "Subscribed",
|
|
224
|
+
"card.windowLimit": "Window limits",
|
|
225
|
+
"card.notApplicable": "Not applicable",
|
|
226
|
+
"card.credits": "Credit balance",
|
|
227
|
+
"card.monthlyRemaining": "Monthly remaining",
|
|
228
|
+
"card.spent": "Spent",
|
|
229
|
+
"card.requests": "Requests",
|
|
230
|
+
"card.account": "Account",
|
|
231
|
+
"card.cycleEnd": "Cycle ends",
|
|
232
|
+
"card.balance": "Balance",
|
|
233
|
+
"card.balanceEmpty": "The endpoint returned no balance_infos",
|
|
234
|
+
"card.available": "Account available",
|
|
235
|
+
"card.unavailable": "Account unavailable",
|
|
236
|
+
"card.deepseekAccount": "DeepSeek account",
|
|
237
|
+
"card.noWallet": "No wallet on this account",
|
|
238
|
+
"card.windowFallback": "Window",
|
|
239
|
+
"card.raw": "Raw response",
|
|
240
|
+
"card.hideRaw": "Hide raw response",
|
|
241
|
+
"card.unrecognized": "These fields are not recognized yet — the raw response is below (send it over to calibrate)",
|
|
242
|
+
"history.title": "Balance spend trend",
|
|
243
|
+
"history.range.today": "Today",
|
|
244
|
+
"history.range.yesterday": "Yesterday",
|
|
245
|
+
"history.range.week": "Last 7 days",
|
|
246
|
+
"history.range.month": "Last 30 days",
|
|
247
|
+
"history.range.custom": "Custom",
|
|
248
|
+
"history.start": "Start date",
|
|
249
|
+
"history.end": "End date",
|
|
250
|
+
"history.apply": "Apply range",
|
|
251
|
+
"history.empty": "Not enough balance samples for this period",
|
|
252
|
+
"history.amount": "Spent",
|
|
253
|
+
"history.clearMonth": "Clear selected month",
|
|
254
|
+
"history.confirmClearMonth": "Clear balance records for {month}?",
|
|
255
|
+
"history.reset": "Reset current records",
|
|
256
|
+
"history.month": "Data month",
|
|
257
|
+
"history.noMonths": "No stored months to clear",
|
|
258
|
+
"history.confirmReset": "Clear all balance records for this account?",
|
|
259
|
+
"card.used": "Used",
|
|
260
|
+
"card.total": "Total",
|
|
261
|
+
"window.fiveHour": "5-hour window",
|
|
262
|
+
"window.weekly": "7-day window",
|
|
263
|
+
"window.usedReset": "{percent}% used · resets in {duration}",
|
|
264
|
+
"window.reset": "Window reset",
|
|
265
|
+
"duration.reset": "reset",
|
|
266
|
+
"currency.CNY": "Chinese yuan (CNY)",
|
|
267
|
+
"meta.model": "Model",
|
|
268
|
+
"meta.credential": "Credential",
|
|
269
|
+
"meta.accountLogin": "Account sign-in (no API key)",
|
|
270
|
+
"meta.usageUrl": "Usage page",
|
|
271
|
+
"kind.commandcode": "Command Code usage (windows / credits)",
|
|
272
|
+
"kind.deepseek": "DeepSeek official balance",
|
|
273
|
+
"kind.deepseek-account": "DeepSeek account wallet balance",
|
|
274
|
+
"kind.opencode-go": "OpenCode Go subscription usage",
|
|
275
|
+
"kind.openrouter": "OpenRouter credits",
|
|
276
|
+
"kind.moonshot": "Moonshot / Kimi balance",
|
|
277
|
+
"kind.siliconflow": "SiliconFlow balance",
|
|
278
|
+
"kind.newapi": "New API compatible credits",
|
|
279
|
+
"kind.none": "Usage query not supported",
|
|
280
|
+
"state.noHost": "Cannot reach this machine's host routes: the plugin may not be mounted correctly (see Settings → Plugins)",
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Substitute `{name}` placeholders ourselves so the panel does not depend
|
|
285
|
+
* on the translate function's parameter form: `t(key)` returns the active
|
|
286
|
+
* language's template, and anything it cannot fill stays visible.
|
|
287
|
+
*/
|
|
288
|
+
function fill(template, params) {
|
|
289
|
+
return String(template).replace(/\{(\w+)\}/g, (match, name) => (
|
|
290
|
+
params !== undefined && params[name] !== undefined ? String(params[name]) : match
|
|
291
|
+
));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The localized name of one usage adapter. The host reports a machine
|
|
296
|
+
* `kind`; a kind this plugin does not know yet falls back to the host's own
|
|
297
|
+
* label instead of showing a dictionary key.
|
|
298
|
+
*/
|
|
299
|
+
function kindLabel(t, kind, fallback) {
|
|
300
|
+
if (kind === null || kind === undefined) return t("kind.none");
|
|
301
|
+
const key = `kind.${kind}`;
|
|
302
|
+
const text = t(key);
|
|
303
|
+
return text === key ? String(fallback ?? kind) : text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
// Owned stylesheet. Rendered as part of the cell (see UsageAction) so the
|
|
308
|
+
// slot system removes it with this entry; every color comes from a theme
|
|
309
|
+
// token the running build lists, except the modal scrim, for which the
|
|
310
|
+
// theme exposes no token.
|
|
311
|
+
const STYLES = [
|
|
312
|
+
"@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }",
|
|
313
|
+
"@keyframes dsh-usage-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }",
|
|
314
|
+
".dsh-usage-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 6px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; }",
|
|
315
|
+
".dsh-usage-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
316
|
+
".dsh-usage-btn:active { transform: scale(0.98); }",
|
|
317
|
+
".dsh-usage-btn-busy::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-success-primary); animation: dsh-usage-pulse 1.2s ease-in-out infinite; }",
|
|
318
|
+
".dsh-usage-action { display: inline-flex; align-items: center; justify-content: center; min-height: 32px; padding: 0 13px; border: 1px solid transparent; border-radius: 9px; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; }",
|
|
319
|
+
".dsh-usage-action:active { transform: scale(0.98); }",
|
|
320
|
+
".dsh-usage-action-primary { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); }",
|
|
321
|
+
".dsh-usage-action-outline { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); }",
|
|
322
|
+
".dsh-usage-action-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }",
|
|
323
|
+
".dsh-usage-action-outline:hover, .dsh-usage-action-ghost:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
324
|
+
".dsh-usage-action-ghost:hover { color: var(--dsw-alias-label-primary); }",
|
|
325
|
+
".dsh-usage-modal-overlay { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgba(8, 12, 18, 0.34); backdrop-filter: blur(12px) saturate(125%); -webkit-backdrop-filter: blur(12px) saturate(125%); }",
|
|
326
|
+
".dsh-usage-modal-dialog { display: flex; flex-direction: column; width: min(620px, 100%); max-height: 100%; border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.1); outline: none; overflow: hidden; }",
|
|
327
|
+
".dsh-usage-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px 13px; border-bottom: 1px solid var(--dsw-alias-border-l2); }",
|
|
328
|
+
".dsh-usage-modal-title { font-size: 15px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); }",
|
|
329
|
+
".dsh-usage-modal-close { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 1; cursor: pointer; transition: background 140ms ease; }",
|
|
330
|
+
".dsh-usage-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
|
|
331
|
+
".dsh-usage-modal-content { padding: 14px 18px; overflow: hidden; }",
|
|
332
|
+
".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px 16px; border-top: 1px solid var(--dsw-alias-border-l2); }",
|
|
333
|
+
".dsh-usage-state { padding: 12px 0; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); }",
|
|
334
|
+
".dsh-usage-error { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; padding: 4px 0; }",
|
|
335
|
+
".dsh-usage-error-text { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); word-break: break-all; }",
|
|
336
|
+
".dsh-usage-inline-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); word-break: break-all; }",
|
|
337
|
+
".dsh-usage-stack { display: flex; flex-direction: column; gap: 12px; }",
|
|
338
|
+
".dsh-usage-status-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
339
|
+
".dsh-usage-status { display: inline-flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 auto; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }",
|
|
340
|
+
".dsh-usage-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
|
|
341
|
+
".dsh-usage-status-actions { display: inline-flex; align-items: center; gap: 6px; }",
|
|
342
|
+
".dsh-usage-spinner { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--dsw-alias-border-l2); border-top-color: var(--dsw-alias-label-primary); animation: dsh-usage-spin 0.8s linear infinite; }",
|
|
343
|
+
".dsh-usage-chip { display: inline-flex; align-items: center; height: 28px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease; }",
|
|
344
|
+
".dsh-usage-chip:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
345
|
+
".dsh-usage-chip-active { color: #FFFFFF; border-color: #0A84FF; background: #0A84FF; }",
|
|
346
|
+
".dsh-usage-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 230px; }",
|
|
347
|
+
".dsh-usage-modelbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }",
|
|
348
|
+
".dsh-usage-modelbar-label { flex: 0 0 auto; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
349
|
+
".dsh-usage-balance-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 11px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.035); }",
|
|
350
|
+
".dsh-usage-balance-card-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 14px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); }",
|
|
351
|
+
".dsh-usage-balance-status { font-size: 12px; font-weight: 400; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
352
|
+
".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 25px; }",
|
|
353
|
+
".dsh-usage-balance-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
354
|
+
".dsh-usage-balance-value { font-size: 13px; line-height: 20px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
|
|
355
|
+
".dsh-usage-refresh { display: flex; justify-content: flex-end; }",
|
|
356
|
+
".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 12px; display: flex; flex-direction: column; gap: 6px; }",
|
|
357
|
+
".dsh-usage-meta-row { display: flex; gap: 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
358
|
+
".dsh-usage-meta-key { flex: 0 0 auto; }",
|
|
359
|
+
".dsh-usage-meta-val { flex: 1 1 auto; word-break: break-all; font-variant-numeric: tabular-nums; }",
|
|
360
|
+
".dsh-usage-form { display: flex; flex-direction: column; gap: 14px; padding: 4px 0; }",
|
|
361
|
+
".dsh-usage-field { display: flex; flex-direction: column; gap: 6px; }",
|
|
362
|
+
".dsh-usage-field-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
363
|
+
".dsh-usage-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
364
|
+
".dsh-usage-input { height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; line-height: 20px; }",
|
|
365
|
+
".dsh-usage-mode-toggle { display: flex; flex-wrap: wrap; gap: 8px; }",
|
|
366
|
+
".dsh-usage-mode-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; padding: 0 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 12px; font-weight: 500; line-height: 18px; cursor: pointer; transition: background 140ms ease, color 140ms ease, border-color 140ms ease; }",
|
|
367
|
+
".dsh-usage-mode-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
368
|
+
".dsh-usage-mode-btn-active { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; }",
|
|
369
|
+
".dsh-usage-cc-window { display: flex; flex-direction: column; gap: 6px; }",
|
|
370
|
+
".dsh-usage-cc-window-head { display: flex; align-items: baseline; justify-content: space-between; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
371
|
+
".dsh-usage-cc-window-val { font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
|
|
372
|
+
".dsh-usage-cc-bar { height: 8px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }",
|
|
373
|
+
".dsh-usage-cc-bar-fill { height: 100%; border-radius: 4px; background: var(--dsw-alias-state-success-primary); }",
|
|
374
|
+
".dsh-usage-cc-bar-fill-warn { background: var(--dsw-alias-state-warn-primary); }",
|
|
375
|
+
".dsh-usage-cc-bar-fill-danger { background: var(--dsw-alias-state-error-primary); }",
|
|
376
|
+
".dsh-usage-cc-reset { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }",
|
|
377
|
+
".dsh-usage-history { display: flex; flex-direction: column; gap: 10px; }",
|
|
378
|
+
".dsh-usage-history-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
379
|
+
".dsh-usage-history-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }",
|
|
380
|
+
".dsh-usage-history-ranges { display: inline-flex; align-self: flex-start; gap: 2px; padding: 3px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); flex-wrap: wrap; }",
|
|
381
|
+
".dsh-usage-history-ranges .dsh-usage-mode-btn { min-height: 27px; padding: 0 10px; border-color: transparent; border-radius: 7px; background: transparent; font-size: 11px; }",
|
|
382
|
+
".dsh-usage-history-ranges .dsh-usage-mode-btn-active { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.14); }",
|
|
383
|
+
".dsh-usage-history-form { display: flex; align-items: end; gap: 6px; flex-wrap: wrap; }",
|
|
384
|
+
".dsh-usage-history-date { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
|
|
385
|
+
".dsh-usage-history-date input, .dsh-usage-history-month { height: 30px; min-width: 112px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 12px; }",
|
|
386
|
+
".dsh-usage-history-date input:focus, .dsh-usage-history-month:focus, .dsh-usage-input:focus { outline: 2px solid rgba(10, 132, 255, 0.45); outline-offset: 1px; }",
|
|
387
|
+
".dsh-usage-history-manage { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }",
|
|
388
|
+
".dsh-usage-history-manage .dsh-usage-action { height: 28px; padding: 0 9px; font-size: 11px; }",
|
|
389
|
+
".dsh-usage-history-chart { width: 100%; height: 148px; overflow: visible; }",
|
|
390
|
+
".dsh-usage-history-empty { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 8px 0; }",
|
|
391
|
+
".dsh-usage-modal-body { max-height: max(140px, calc(100vh - 260px)); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2) transparent; }",
|
|
392
|
+
".dsh-usage-provider-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }",
|
|
393
|
+
".dsh-usage-provider-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; }",
|
|
394
|
+
".dsh-usage-provider-row + .dsh-usage-provider-row { border-top: 1px solid var(--dsw-alias-border-l2); }",
|
|
395
|
+
".dsh-usage-provider-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }",
|
|
396
|
+
".dsh-usage-provider-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); word-break: break-all; }",
|
|
397
|
+
".dsh-usage-provider-toggle { display: flex; justify-content: center; }",
|
|
398
|
+
".dsh-usage-raw { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-secondary); word-break: break-all; max-height: 140px; overflow: auto; border-radius: 8px; padding: 8px 10px; background: var(--dsw-alias-bg-layer-2); }",
|
|
399
|
+
].join("\n");
|
|
400
|
+
|
|
401
|
+
const CONFIG_KEY = "dsh-model-usage:refresh";
|
|
402
|
+
/** Key this plugin used before it was renamed to dsh-model-usage. */
|
|
403
|
+
const LEGACY_CONFIG_KEY = "dsh-desktop-usage:refresh";
|
|
404
|
+
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
|
|
405
|
+
const MIN_INTERVAL_MS = 60 * 1000;
|
|
406
|
+
const MAX_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
407
|
+
/** Offered intervals, in minutes; the input next to them takes any 1–1440. */
|
|
408
|
+
const INTERVAL_PRESETS = [1, 5, 15, 30, 60];
|
|
409
|
+
/** How often the open panel re-reads the clock and re-checks staleness. */
|
|
410
|
+
const TICK_MS = 5000;
|
|
411
|
+
const HISTORY_KEY = "dsh-model-usage:commandcode-history";
|
|
412
|
+
const HISTORY_INTERVAL_MS = 10 * 60 * 1000;
|
|
413
|
+
const HISTORY_RANGES = ["today", "yesterday", "week", "month", "custom"];
|
|
414
|
+
/** Everything Tab may land on inside a dialog, for the focus trap. */
|
|
415
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
416
|
+
|
|
417
|
+
function clampInterval(ms) {
|
|
418
|
+
if (!Number.isFinite(ms) || ms <= 0) return DEFAULT_INTERVAL_MS;
|
|
419
|
+
return Math.min(MAX_INTERVAL_MS, Math.max(MIN_INTERVAL_MS, Math.round(ms)));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Read the saved refresh policy; anything malformed falls back to the default. */
|
|
423
|
+
function readRefreshConfig() {
|
|
424
|
+
const fallback = { auto: true, intervalMs: DEFAULT_INTERVAL_MS };
|
|
425
|
+
try {
|
|
426
|
+
// The pre-rename key is still honoured so a rename does not silently
|
|
427
|
+
// reset the policy someone already chose.
|
|
428
|
+
const raw = localStorage.getItem(CONFIG_KEY) ?? localStorage.getItem(LEGACY_CONFIG_KEY);
|
|
429
|
+
if (raw == null) return fallback;
|
|
430
|
+
const parsed = JSON.parse(raw);
|
|
431
|
+
if (parsed === null || typeof parsed !== "object") return fallback;
|
|
432
|
+
return {
|
|
433
|
+
auto: parsed.auto !== false,
|
|
434
|
+
intervalMs: clampInterval(Number(parsed.intervalMs)),
|
|
435
|
+
};
|
|
436
|
+
} catch {
|
|
437
|
+
// Corrupt storage (quota/private mode) degrades to the default policy.
|
|
438
|
+
return fallback;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Persist the refresh policy; storage failures keep the in-memory value only. */
|
|
443
|
+
function writeRefreshConfig(config) {
|
|
444
|
+
try {
|
|
445
|
+
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
|
446
|
+
} catch {
|
|
447
|
+
// Quota/private mode: the policy still applies for this session.
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function historyStorageKey(data) {
|
|
452
|
+
if (data?.mode !== "commandcode") return null;
|
|
453
|
+
const account = typeof data.data?.account === "string" && data.data.account !== ""
|
|
454
|
+
? data.data.account
|
|
455
|
+
: `${data.provider ?? "commandcode"}:${data.credential?.ref ?? "default"}:${data.credential?.source ?? ""}`;
|
|
456
|
+
return `${HISTORY_KEY}:${encodeURIComponent(account)}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Keep the current calendar month plus the two preceding complete months.
|
|
460
|
+
// In October this retains October, September and August, and removes July.
|
|
461
|
+
function historyCutoff(value = Date.now()) {
|
|
462
|
+
const date = new Date(value);
|
|
463
|
+
return new Date(date.getFullYear(), date.getMonth() - 2, 1).getTime();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function readHistory(data) {
|
|
467
|
+
const key = historyStorageKey(data);
|
|
468
|
+
if (key === null) return [];
|
|
469
|
+
try {
|
|
470
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? "[]");
|
|
471
|
+
if (!Array.isArray(parsed)) return [];
|
|
472
|
+
const now = Date.now();
|
|
473
|
+
const cutoff = historyCutoff(now);
|
|
474
|
+
const retained = parsed.filter((point) => point !== null && typeof point === "object"
|
|
475
|
+
&& Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
|
|
476
|
+
&& Number.isFinite(point.balance)
|
|
477
|
+
&& (point.spend === null || Number.isFinite(point.spend)))
|
|
478
|
+
.sort((a, b) => a.at - b.at);
|
|
479
|
+
if (retained.length !== parsed.length) {
|
|
480
|
+
try {
|
|
481
|
+
localStorage.setItem(key, JSON.stringify(retained));
|
|
482
|
+
} catch {
|
|
483
|
+
// The chart still uses the filtered in-memory history.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return retained;
|
|
487
|
+
} catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function pruneStoredHistories(now = Date.now()) {
|
|
493
|
+
const prefix = `${HISTORY_KEY}:`;
|
|
494
|
+
const cutoff = historyCutoff(now);
|
|
495
|
+
try {
|
|
496
|
+
for (let index = localStorage.length - 1; index >= 0; index -= 1) {
|
|
497
|
+
const key = localStorage.key(index);
|
|
498
|
+
if (typeof key !== "string" || !key.startsWith(prefix)) continue;
|
|
499
|
+
let parsed;
|
|
500
|
+
try { parsed = JSON.parse(localStorage.getItem(key) ?? "[]"); } catch { parsed = []; }
|
|
501
|
+
const retained = Array.isArray(parsed)
|
|
502
|
+
? parsed.filter((point) => point !== null && typeof point === "object"
|
|
503
|
+
&& Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
|
|
504
|
+
&& Number.isFinite(point.balance)
|
|
505
|
+
&& (point.spend === null || Number.isFinite(point.spend)))
|
|
506
|
+
: [];
|
|
507
|
+
if (retained.length === 0) localStorage.removeItem(key);
|
|
508
|
+
else if (retained.length !== parsed.length) localStorage.setItem(key, JSON.stringify(retained));
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// A storage restriction must not stop balance queries.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function recordHistorySample(data, now = Date.now()) {
|
|
516
|
+
const key = historyStorageKey(data);
|
|
517
|
+
const balance = Number(data?.data?.credits?.totalRemaining);
|
|
518
|
+
if (key === null || !Number.isFinite(balance)) return [];
|
|
519
|
+
pruneStoredHistories(now);
|
|
520
|
+
const points = readHistory(data);
|
|
521
|
+
const last = points[points.length - 1];
|
|
522
|
+
if (last !== undefined && Math.floor(last.at / HISTORY_INTERVAL_MS) === Math.floor(now / HISTORY_INTERVAL_MS)) return points;
|
|
523
|
+
points.push({
|
|
524
|
+
at: now,
|
|
525
|
+
balance,
|
|
526
|
+
spend: last === undefined ? null : Math.max(0, last.balance - balance),
|
|
527
|
+
});
|
|
528
|
+
const retained = points.filter((point) => point.at >= historyCutoff(now));
|
|
529
|
+
try {
|
|
530
|
+
localStorage.setItem(key, JSON.stringify(retained));
|
|
531
|
+
} catch {
|
|
532
|
+
// Storage can be unavailable or full; the current session still renders.
|
|
533
|
+
}
|
|
534
|
+
return retained;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function storedMonths(points) {
|
|
538
|
+
return [...new Set(points.map((point) => {
|
|
539
|
+
const date = new Date(point.at);
|
|
540
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
541
|
+
}))].sort().reverse();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function clearHistoryMonth(data, month) {
|
|
545
|
+
const key = historyStorageKey(data);
|
|
546
|
+
if (key === null || !/^\d{4}-\d{2}$/.test(month)) return [];
|
|
547
|
+
const points = readHistory(data).filter((point) => {
|
|
548
|
+
const date = new Date(point.at);
|
|
549
|
+
const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
550
|
+
return value !== month;
|
|
551
|
+
});
|
|
552
|
+
try { localStorage.setItem(key, JSON.stringify(points)); } catch { /* memory view remains usable */ }
|
|
553
|
+
return points;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function resetHistoryStorage(data) {
|
|
557
|
+
const key = historyStorageKey(data);
|
|
558
|
+
if (key !== null) {
|
|
559
|
+
try { localStorage.removeItem(key); } catch { /* memory view remains usable */ }
|
|
560
|
+
}
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const MODEL_KEY = "dsh-model-usage:model";
|
|
565
|
+
/** Key this plugin used before it was renamed to dsh-model-usage. */
|
|
566
|
+
const LEGACY_MODEL_KEY = "dsh-desktop-usage:model";
|
|
567
|
+
|
|
568
|
+
/** Read the manual model override; absent or malformed means "follow the conversation". */
|
|
569
|
+
function readModelOverride() {
|
|
570
|
+
try {
|
|
571
|
+
const raw = localStorage.getItem(MODEL_KEY) ?? localStorage.getItem(LEGACY_MODEL_KEY);
|
|
572
|
+
if (raw == null) return null;
|
|
573
|
+
const parsed = JSON.parse(raw);
|
|
574
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
575
|
+
if (typeof parsed.provider !== "string" || parsed.provider === "") return null;
|
|
576
|
+
return {
|
|
577
|
+
provider: parsed.provider,
|
|
578
|
+
model: typeof parsed.model === "string" ? parsed.model : "",
|
|
579
|
+
};
|
|
580
|
+
} catch {
|
|
581
|
+
// Corrupt storage degrades to following the conversation.
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Persist the override (`null` restores 自动). */
|
|
587
|
+
function writeModelOverride(override) {
|
|
588
|
+
try {
|
|
589
|
+
if (override === null) localStorage.removeItem(MODEL_KEY);
|
|
590
|
+
else localStorage.setItem(MODEL_KEY, JSON.stringify(override));
|
|
591
|
+
} catch {
|
|
592
|
+
// Quota/private mode: the choice still applies for this session.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function minutesLabel(t, ms) {
|
|
597
|
+
const minutes = ms / 60000;
|
|
598
|
+
const value = Number.isInteger(minutes) ? String(minutes) : minutes.toFixed(1);
|
|
599
|
+
return fill(t("minutes.value"), { count: value });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Human age of one refresh: 刚刚 / N 分钟前 / N 小时前 / N 天前. */
|
|
603
|
+
function formatAge(t, ms) {
|
|
604
|
+
if (!Number.isFinite(ms) || ms < 60000) return t("age.now");
|
|
605
|
+
const minutes = Math.floor(ms / 60000);
|
|
606
|
+
if (minutes < 60) return fill(t("age.minutes"), { count: minutes });
|
|
607
|
+
const hours = Math.floor(minutes / 60);
|
|
608
|
+
if (hours < 24) return fill(t("age.hours"), { count: hours });
|
|
609
|
+
return fill(t("age.days"), { count: Math.floor(hours / 24) });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function currencyName(t, currency) {
|
|
613
|
+
return currency === "CNY" ? t("currency.CNY") : String(currency);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function currencySymbol(currency) {
|
|
617
|
+
return currency === "CNY" ? "¥" : `${currency} `;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function unitSymbol(unit) {
|
|
621
|
+
if (unit === "CNY") return "¥";
|
|
622
|
+
if (unit === "USD") return "$";
|
|
623
|
+
return `${unit} `;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function row(label, value, key) {
|
|
627
|
+
return React.createElement("div", { key, className: "dsh-usage-balance-row" },
|
|
628
|
+
React.createElement("span", { className: "dsh-usage-balance-label" }, label),
|
|
629
|
+
React.createElement("span", { className: "dsh-usage-balance-value" }, value),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// -----------------------------------------------------------------------
|
|
634
|
+
// Own controls. Copying the shipped primitives' markup is not an option for
|
|
635
|
+
// an out-of-tree bundle (that package is a build-time dependency), so these
|
|
636
|
+
// are this plugin's own: they use only theme tokens, keep the behaviors
|
|
637
|
+
// users rely on (Escape to close, focus moved in and restored on close,
|
|
638
|
+
// Tab cycling inside the dialog), and carry their accessible names.
|
|
639
|
+
|
|
640
|
+
const ACTION_VARIANTS = {
|
|
641
|
+
primary: "dsh-usage-action-primary",
|
|
642
|
+
outline: "dsh-usage-action-outline",
|
|
643
|
+
ghost: "dsh-usage-action-ghost",
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/** One dialog action button: variant + native button props. */
|
|
647
|
+
function ActionButton(props) {
|
|
648
|
+
const { variant, className, children } = props;
|
|
649
|
+
const rest = { ...props };
|
|
650
|
+
delete rest.variant;
|
|
651
|
+
delete rest.className;
|
|
652
|
+
delete rest.children;
|
|
653
|
+
const variantClass = ACTION_VARIANTS[variant] ?? ACTION_VARIANTS.ghost;
|
|
654
|
+
return React.createElement("button", {
|
|
655
|
+
type: "button",
|
|
656
|
+
...rest,
|
|
657
|
+
className: "dsh-usage-action " + variantClass + (className ? ` ${className}` : ""),
|
|
658
|
+
}, children);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* One modal dialog. `onClose` is held in a ref so the focus effect runs only
|
|
663
|
+
* on open/close — the panel re-renders on every background refresh, and
|
|
664
|
+
* re-running it there would steal focus every few seconds.
|
|
665
|
+
*/
|
|
666
|
+
function UsageModal(props) {
|
|
667
|
+
const { open, onClose, title, closeLabel, className, footer, children } = props;
|
|
668
|
+
const dialogRef = React.useRef(null);
|
|
669
|
+
const restoreRef = React.useRef(null);
|
|
670
|
+
const closeRef = React.useRef(onClose);
|
|
671
|
+
closeRef.current = onClose;
|
|
672
|
+
|
|
673
|
+
React.useEffect(() => {
|
|
674
|
+
if (!open) return undefined;
|
|
675
|
+
restoreRef.current = typeof document === "undefined" ? null : document.activeElement;
|
|
676
|
+
const close = () => { closeRef.current(); };
|
|
677
|
+
const onKeyDown = (event) => {
|
|
678
|
+
if (event.key === "Escape") {
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
event.stopPropagation();
|
|
681
|
+
close();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (event.key !== "Tab") return;
|
|
685
|
+
const dialog = dialogRef.current;
|
|
686
|
+
if (dialog === null) return;
|
|
687
|
+
const focusable = dialog.querySelectorAll(FOCUSABLE);
|
|
688
|
+
if (focusable.length === 0) {
|
|
689
|
+
event.preventDefault();
|
|
690
|
+
dialog.focus();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const first = focusable[0];
|
|
694
|
+
const last = focusable[focusable.length - 1];
|
|
695
|
+
const active = document.activeElement;
|
|
696
|
+
const inside = active !== null && dialog.contains(active);
|
|
697
|
+
if (event.shiftKey) {
|
|
698
|
+
if (!inside || active === first) {
|
|
699
|
+
event.preventDefault();
|
|
700
|
+
last.focus();
|
|
701
|
+
}
|
|
702
|
+
} else if (!inside || active === last) {
|
|
703
|
+
event.preventDefault();
|
|
704
|
+
first.focus();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
708
|
+
const timer = window.setTimeout(() => {
|
|
709
|
+
const dialog = dialogRef.current;
|
|
710
|
+
if (dialog === null) return;
|
|
711
|
+
const focusable = dialog.querySelectorAll(FOCUSABLE);
|
|
712
|
+
const target = focusable.length > 0 ? focusable[0] : dialog;
|
|
713
|
+
if (typeof target.focus === "function") target.focus();
|
|
714
|
+
}, 0);
|
|
715
|
+
return () => {
|
|
716
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
717
|
+
window.clearTimeout(timer);
|
|
718
|
+
const restore = restoreRef.current;
|
|
719
|
+
if (restore !== null && typeof restore.focus === "function") restore.focus();
|
|
720
|
+
};
|
|
721
|
+
}, [open]);
|
|
722
|
+
|
|
723
|
+
if (!open) return null;
|
|
724
|
+
return React.createElement("div", {
|
|
725
|
+
className: "dsh-usage-modal-overlay",
|
|
726
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) closeRef.current(); },
|
|
727
|
+
},
|
|
728
|
+
React.createElement("div", {
|
|
729
|
+
ref: dialogRef,
|
|
730
|
+
role: "dialog",
|
|
731
|
+
"aria-modal": "true",
|
|
732
|
+
"aria-label": title,
|
|
733
|
+
tabIndex: -1,
|
|
734
|
+
className: "dsh-usage-modal-dialog" + (className ? ` ${className}` : ""),
|
|
735
|
+
},
|
|
736
|
+
React.createElement("div", { className: "dsh-usage-modal-head" },
|
|
737
|
+
React.createElement("span", { className: "dsh-usage-modal-title" }, title),
|
|
738
|
+
React.createElement("button", {
|
|
739
|
+
type: "button",
|
|
740
|
+
className: "dsh-usage-modal-close",
|
|
741
|
+
"aria-label": closeLabel,
|
|
742
|
+
title: closeLabel,
|
|
743
|
+
onClick: () => { closeRef.current(); },
|
|
744
|
+
}, "×"),
|
|
745
|
+
),
|
|
746
|
+
React.createElement("div", { className: "dsh-usage-modal-content" }, children),
|
|
747
|
+
footer === undefined || footer === null
|
|
748
|
+
? null
|
|
749
|
+
: React.createElement("div", { className: "dsh-usage-modal-footer" }, footer),
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** Format a duration (ms) as "Xh Ym" / "Xd Yh", falling back to "已重置". */
|
|
755
|
+
function formatDuration(t, ms) {
|
|
756
|
+
if (!Number.isFinite(ms) || ms <= 0) return t("duration.reset");
|
|
757
|
+
const totalMin = Math.floor(ms / 60000);
|
|
758
|
+
if (totalMin < 60) return `${totalMin}m`;
|
|
759
|
+
const h = Math.floor(totalMin / 60);
|
|
760
|
+
const m = totalMin % 60;
|
|
761
|
+
if (h < 24) return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
762
|
+
const d = Math.floor(h / 24);
|
|
763
|
+
const rh = h % 24;
|
|
764
|
+
return rh > 0 ? `${d}d ${rh}h` : `${d}d`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** One rolling-window meter: label, used/cap, percentage bar, reset countdown. */
|
|
768
|
+
function CcWindowMeter({ label, used, cap, resetAt, now, t }) {
|
|
769
|
+
const pct = cap > 0 ? Math.min(100, (used / cap) * 100) : 0;
|
|
770
|
+
const barClass = pct >= 90 ? "dsh-usage-cc-bar-fill-danger"
|
|
771
|
+
: pct >= 70 ? "dsh-usage-cc-bar-fill-warn"
|
|
772
|
+
: "dsh-usage-cc-bar-fill";
|
|
773
|
+
const resetIn = resetAt > 0 ? resetAt - now : 0;
|
|
774
|
+
return React.createElement("div", { className: "dsh-usage-cc-window" },
|
|
775
|
+
React.createElement("div", { className: "dsh-usage-cc-window-head" },
|
|
776
|
+
React.createElement("span", null, label),
|
|
777
|
+
React.createElement("span", { className: "dsh-usage-cc-window-val" },
|
|
778
|
+
`$${used.toFixed(2)} / $${cap.toFixed(2)}`),
|
|
779
|
+
),
|
|
780
|
+
React.createElement("div", { className: "dsh-usage-cc-bar" },
|
|
781
|
+
React.createElement("div", { className: barClass, style: { width: `${pct}%` } }),
|
|
782
|
+
),
|
|
783
|
+
React.createElement("div", { className: "dsh-usage-cc-reset" },
|
|
784
|
+
resetIn > 0
|
|
785
|
+
? fill(t("window.usedReset"), { percent: Math.round(pct), duration: formatDuration(t, resetIn) })
|
|
786
|
+
: t("window.reset")),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Command Code route: plan windows + credits, normalized by the host. */
|
|
791
|
+
function CommandCodeCard({ data, t }) {
|
|
792
|
+
const d = data.data;
|
|
793
|
+
const windows = d.windows;
|
|
794
|
+
const credits = d.credits;
|
|
795
|
+
const now = typeof d.now === "number" ? d.now : Date.now();
|
|
796
|
+
const cycleEnd = typeof d.cycleEnd === "number" && d.cycleEnd > 0
|
|
797
|
+
? new Date(d.cycleEnd).toLocaleDateString()
|
|
798
|
+
: null;
|
|
799
|
+
return React.createElement("div", { className: "dsh-usage-balance-card" },
|
|
800
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" },
|
|
801
|
+
String(d.planName),
|
|
802
|
+
React.createElement("span", { className: "dsh-usage-balance-status" },
|
|
803
|
+
d.status === "active" ? t("card.subscribed") : String(d.status ?? "")),
|
|
804
|
+
),
|
|
805
|
+
windows !== null && windows.limited
|
|
806
|
+
? React.createElement("div", { className: "dsh-usage-stack" },
|
|
807
|
+
windows.fiveHour !== null && windows.fiveHour.cap > 0
|
|
808
|
+
? React.createElement(CcWindowMeter, { label: t("window.fiveHour"), ...windows.fiveHour, now, t })
|
|
809
|
+
: null,
|
|
810
|
+
windows.weekly !== null && windows.weekly.cap > 0
|
|
811
|
+
? React.createElement(CcWindowMeter, { label: t("window.weekly"), ...windows.weekly, now, t })
|
|
812
|
+
: null,
|
|
813
|
+
)
|
|
814
|
+
: row(t("card.windowLimit"), t("card.notApplicable"), "cc-window"),
|
|
815
|
+
React.createElement("div", { className: "dsh-usage-stack" },
|
|
816
|
+
row(t("card.credits"), `$${(credits.totalRemaining ?? 0).toFixed(2)}`, "cc-total"),
|
|
817
|
+
row(t("card.monthlyRemaining"), `$${(credits.monthlyRemaining ?? 0).toFixed(2)}`, "cc-monthly"),
|
|
818
|
+
row(t("card.spent"), `$${(credits.totalSpent ?? 0).toFixed(2)}`, "cc-spent"),
|
|
819
|
+
row(t("card.requests"), String(credits.totalCount ?? 0), "cc-count"),
|
|
820
|
+
d.account !== null && d.account !== undefined ? row(t("card.account"), String(d.account), "cc-account") : null,
|
|
821
|
+
cycleEnd !== null ? row(t("card.cycleEnd"), cycleEnd, "cc-cycle") : null,
|
|
822
|
+
),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function dateInputValue(value) {
|
|
827
|
+
const date = new Date(value);
|
|
828
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function localDateStart(value) {
|
|
832
|
+
const [year, month, day] = String(value).split("-").map(Number);
|
|
833
|
+
return Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)
|
|
834
|
+
? new Date(year, month - 1, day).getTime()
|
|
835
|
+
: NaN;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function BalanceHistory({ points, range, customRange, appliedCustomRange, selectedMonth, onRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onReset, t }) {
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
const todayStart = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
|
|
841
|
+
let start = todayStart;
|
|
842
|
+
let end = todayStart + 24 * 60 * 60 * 1000;
|
|
843
|
+
let daily = false;
|
|
844
|
+
if (range === "yesterday") {
|
|
845
|
+
start -= 24 * 60 * 60 * 1000;
|
|
846
|
+
end -= 24 * 60 * 60 * 1000;
|
|
847
|
+
} else if (range === "week") {
|
|
848
|
+
start -= 6 * 24 * 60 * 60 * 1000;
|
|
849
|
+
daily = true;
|
|
850
|
+
} else if (range === "month") {
|
|
851
|
+
start -= 29 * 24 * 60 * 60 * 1000;
|
|
852
|
+
daily = true;
|
|
853
|
+
} else if (range === "custom") {
|
|
854
|
+
const customStart = localDateStart(appliedCustomRange.start);
|
|
855
|
+
const customEnd = localDateStart(appliedCustomRange.end);
|
|
856
|
+
if (Number.isFinite(customStart) && Number.isFinite(customEnd) && customEnd >= customStart) {
|
|
857
|
+
start = customStart;
|
|
858
|
+
end = customEnd + 24 * 60 * 60 * 1000;
|
|
859
|
+
daily = end - start >= 7 * 24 * 60 * 60 * 1000;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const rawVisible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
|
|
863
|
+
let visible = rawVisible;
|
|
864
|
+
if (daily) {
|
|
865
|
+
const buckets = new Map();
|
|
866
|
+
for (const point of rawVisible) {
|
|
867
|
+
const date = new Date(point.at);
|
|
868
|
+
const bucket = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
869
|
+
buckets.set(bucket, (buckets.get(bucket) ?? 0) + point.spend);
|
|
870
|
+
}
|
|
871
|
+
visible = [...buckets.entries()].map(([at, spend]) => ({ at: at + 12 * 60 * 60 * 1000, spend })).sort((a, b) => a.at - b.at);
|
|
872
|
+
}
|
|
873
|
+
const maxSpend = Math.max(0.01, ...visible.map((point) => point.spend));
|
|
874
|
+
const chartPoints = visible.map((point) => ({
|
|
875
|
+
x: 42 + Math.max(0, Math.min(1, (point.at - start) / (end - start))) * 306,
|
|
876
|
+
y: 104 - (point.spend / maxSpend) * 78,
|
|
877
|
+
point,
|
|
878
|
+
}));
|
|
879
|
+
const sum = visible.reduce((total, point) => total + point.spend, 0);
|
|
880
|
+
const span = end - start;
|
|
881
|
+
const tickCount = span <= 2 * 24 * 60 * 60 * 1000 ? 7 : span <= 8 * 24 * 60 * 60 * 1000 ? 8 : 7;
|
|
882
|
+
const tickLabel = (value) => span <= 2 * 24 * 60 * 60 * 1000
|
|
883
|
+
? new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
884
|
+
: new Date(value).toLocaleDateString([], { month: "2-digit", day: "2-digit" });
|
|
885
|
+
const children = [];
|
|
886
|
+
for (let index = 0; index < tickCount; index += 1) {
|
|
887
|
+
const ratio = index / (tickCount - 1);
|
|
888
|
+
const x = 42 + ratio * 306;
|
|
889
|
+
const value = start + ratio * span;
|
|
890
|
+
children.push(
|
|
891
|
+
React.createElement("line", { key: `grid-${index}`, x1: x, x2: x, y1: 20, y2: 108, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1 }),
|
|
892
|
+
React.createElement("text", { key: `tick-${index}`, x, y: 126, textAnchor: index === 0 ? "start" : index === tickCount - 1 ? "end" : "middle", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, tickLabel(value)),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
children.push(
|
|
896
|
+
React.createElement("text", { key: "y-max", x: 36, y: 25, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, `$${maxSpend.toFixed(2)}`),
|
|
897
|
+
React.createElement("text", { key: "y-zero", x: 36, y: 108, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, "$0"),
|
|
898
|
+
);
|
|
899
|
+
if (chartPoints.length > 0) {
|
|
900
|
+
children.push(React.createElement("polyline", {
|
|
901
|
+
key: "line", points: chartPoints.map(({ x, y }) => `${x},${y}`).join(" "), fill: "none",
|
|
902
|
+
stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round",
|
|
903
|
+
}));
|
|
904
|
+
chartPoints.forEach(({ x, y, point }, index) => children.push(
|
|
905
|
+
React.createElement("g", { key: `point-${index}` },
|
|
906
|
+
React.createElement("circle", { cx: x, cy: y, r: 3.5, fill: "var(--dsw-alias-state-success-primary)" }),
|
|
907
|
+
React.createElement("title", null, `${new Date(point.at).toLocaleString()} · $${point.spend.toFixed(2)}`),
|
|
908
|
+
),
|
|
909
|
+
));
|
|
910
|
+
}
|
|
911
|
+
const months = storedMonths(points);
|
|
912
|
+
const activeMonth = months.includes(selectedMonth) ? selectedMonth : (months[0] ?? "");
|
|
913
|
+
return React.createElement("div", { className: "dsh-usage-balance-card dsh-usage-history" },
|
|
914
|
+
React.createElement("div", { className: "dsh-usage-history-head" },
|
|
915
|
+
React.createElement("span", { className: "dsh-usage-history-title" }, t("history.title")),
|
|
916
|
+
visible.length > 0 ? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`) : null,
|
|
917
|
+
),
|
|
918
|
+
React.createElement("div", { className: "dsh-usage-history-ranges" }, HISTORY_RANGES.map((item) =>
|
|
919
|
+
React.createElement("button", { key: item, type: "button", className: "dsh-usage-mode-btn" + (range === item ? " dsh-usage-mode-btn-active" : ""), onClick: () => onRange(item) }, t(`history.range.${item}`)))),
|
|
920
|
+
range === "custom"
|
|
921
|
+
? React.createElement("div", { className: "dsh-usage-history-form" },
|
|
922
|
+
React.createElement("label", { className: "dsh-usage-history-date" }, t("history.start"), React.createElement("input", { type: "date", value: customRange.start, onChange: (event) => onCustomRange({ ...customRange, start: event.target.value }) })),
|
|
923
|
+
React.createElement("label", { className: "dsh-usage-history-date" }, t("history.end"), React.createElement("input", { type: "date", value: customRange.end, onChange: (event) => onCustomRange({ ...customRange, end: event.target.value }) })),
|
|
924
|
+
React.createElement(ActionButton, { variant: "outline", onClick: onApplyCustomRange }, t("history.apply")),
|
|
925
|
+
)
|
|
926
|
+
: null,
|
|
927
|
+
visible.length > 0
|
|
928
|
+
? React.createElement("svg", { className: "dsh-usage-history-chart", viewBox: "0 0 360 132", role: "img", "aria-label": t("history.title") }, children)
|
|
929
|
+
: React.createElement("div", { className: "dsh-usage-history-empty" }, t("history.empty")),
|
|
930
|
+
React.createElement("div", { className: "dsh-usage-history-manage" },
|
|
931
|
+
React.createElement("span", { className: "dsh-usage-history-title" }, t("history.month")),
|
|
932
|
+
months.length > 0
|
|
933
|
+
? React.createElement("select", { className: "dsh-usage-history-month", value: activeMonth, onChange: (event) => onSelectMonth(event.target.value) }, months.map((month) => React.createElement("option", { key: month, value: month }, month)))
|
|
934
|
+
: React.createElement("span", { className: "dsh-usage-history-empty" }, t("history.noMonths")),
|
|
935
|
+
months.length > 0 ? React.createElement(ActionButton, { variant: "ghost", onClick: () => onClearMonth(activeMonth) }, t("history.clearMonth")) : null,
|
|
936
|
+
React.createElement(ActionButton, { variant: "ghost", onClick: onReset }, t("history.reset")),
|
|
937
|
+
),
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Official DeepSeek route: one card per currency, 余额 only. */
|
|
942
|
+
function DeepSeekCard({ data, t }) {
|
|
943
|
+
const infos = Array.isArray(data.data.balance_infos) ? data.data.balance_infos : [];
|
|
944
|
+
if (infos.length === 0) {
|
|
945
|
+
return React.createElement("div", { className: "dsh-usage-balance-card" },
|
|
946
|
+
row(t("card.balance"), t("card.balanceEmpty"), "balance-empty"));
|
|
947
|
+
}
|
|
948
|
+
return React.createElement("div", { className: "dsh-usage-stack" },
|
|
949
|
+
infos.map((info) => React.createElement(
|
|
950
|
+
"div",
|
|
951
|
+
{ key: info.currency, className: "dsh-usage-balance-card" },
|
|
952
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" },
|
|
953
|
+
currencyName(t, info.currency),
|
|
954
|
+
React.createElement("span", { className: "dsh-usage-balance-status" },
|
|
955
|
+
data.data.is_available === true ? t("card.available") : t("card.unavailable")),
|
|
956
|
+
),
|
|
957
|
+
row(t("card.balance"), currencySymbol(info.currency) + String(info.total_balance), "balance"),
|
|
958
|
+
)));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** DeepSeek account route: platform wallets (recharge + bonus). */
|
|
962
|
+
function DeepSeekAccountCard({ data, t }) {
|
|
963
|
+
const wallets = Array.isArray(data.data.wallets) ? data.data.wallets : [];
|
|
964
|
+
return React.createElement("div", { className: "dsh-usage-balance-card" },
|
|
965
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" }, t("card.deepseekAccount")),
|
|
966
|
+
wallets.length === 0
|
|
967
|
+
? row(t("card.balance"), t("card.noWallet"), "account-empty")
|
|
968
|
+
: React.createElement("div", { className: "dsh-usage-stack" },
|
|
969
|
+
wallets.map((wallet, index) => row(
|
|
970
|
+
currencyName(t, wallet.currency),
|
|
971
|
+
currencySymbol(wallet.currency) + String(wallet.balance),
|
|
972
|
+
`wallet-${index}`,
|
|
973
|
+
))),
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Generic provider card for the adapters that normalize to the same two
|
|
979
|
+
* building blocks: `windows` (used / limit / reset meters — OpenCode Go's
|
|
980
|
+
* 5h / weekly / monthly allowances) and `rows` (label → value lines —
|
|
981
|
+
* OpenRouter credits, Moonshot and SiliconFlow balances).
|
|
982
|
+
*
|
|
983
|
+
* Whatever the endpoint returned rides along as `raw` behind a toggle: the
|
|
984
|
+
* OpenCode Go payload is not publicly documented, so an unrecognized field
|
|
985
|
+
* stays visible instead of turning into an empty card.
|
|
986
|
+
*/
|
|
987
|
+
function UsageRowsCard({ data, t }) {
|
|
988
|
+
const [showRaw, setShowRaw] = React.useState(false);
|
|
989
|
+
const d = data.data;
|
|
990
|
+
const windows = Array.isArray(d.windows) ? d.windows : [];
|
|
991
|
+
const models = Array.isArray(d.models) ? d.models : [];
|
|
992
|
+
const rows = Array.isArray(d.rows) ? d.rows : [];
|
|
993
|
+
const now = typeof d.now === "number" ? d.now : Date.now();
|
|
994
|
+
const recognized = windows.length > 0 || models.length > 0 || rows.length > 0;
|
|
995
|
+
const money = (value) => (value === null || value === undefined ? "—" : `$${Number(value).toFixed(2)}`);
|
|
996
|
+
return React.createElement("div", { className: "dsh-usage-balance-card" },
|
|
997
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" },
|
|
998
|
+
String(data.providerName ?? data.provider)),
|
|
999
|
+
windows.length > 0
|
|
1000
|
+
? React.createElement("div", { className: "dsh-usage-stack" },
|
|
1001
|
+
windows.map((w, i) => React.createElement(CcWindowMeter, {
|
|
1002
|
+
key: `w-${i}`,
|
|
1003
|
+
label: String(w.label ?? t("card.windowFallback")),
|
|
1004
|
+
used: Number(w.used ?? 0),
|
|
1005
|
+
cap: Number(w.limit ?? 0),
|
|
1006
|
+
resetAt: Number(w.resetsAt ?? 0),
|
|
1007
|
+
now,
|
|
1008
|
+
t,
|
|
1009
|
+
})))
|
|
1010
|
+
: null,
|
|
1011
|
+
rows.length > 0
|
|
1012
|
+
? React.createElement("div", { className: "dsh-usage-stack" },
|
|
1013
|
+
rows.map((entry, i) => row(String(entry.label), String(entry.value), `r-${i}`)))
|
|
1014
|
+
: null,
|
|
1015
|
+
models.length > 0
|
|
1016
|
+
? React.createElement("div", { className: "dsh-usage-stack" },
|
|
1017
|
+
models.slice(0, 12).map((entry, i) => row(
|
|
1018
|
+
String(entry.model),
|
|
1019
|
+
entry.limit === null || entry.limit === undefined
|
|
1020
|
+
? money(entry.used)
|
|
1021
|
+
: `${money(entry.used)} / ${money(entry.limit)}`,
|
|
1022
|
+
`m-${i}`,
|
|
1023
|
+
)))
|
|
1024
|
+
: null,
|
|
1025
|
+
!recognized
|
|
1026
|
+
? React.createElement("div", { className: "dsh-usage-hint" }, t("card.unrecognized"))
|
|
1027
|
+
: null,
|
|
1028
|
+
d.raw === undefined || d.raw === null
|
|
1029
|
+
? null
|
|
1030
|
+
: React.createElement("div", { className: "dsh-usage-stack" },
|
|
1031
|
+
React.createElement("div", { className: "dsh-usage-provider-toggle" },
|
|
1032
|
+
React.createElement("button", {
|
|
1033
|
+
type: "button",
|
|
1034
|
+
className: "dsh-usage-chip",
|
|
1035
|
+
onClick: () => { setShowRaw(!showRaw); },
|
|
1036
|
+
}, showRaw ? t("card.hideRaw") : t("card.raw"))),
|
|
1037
|
+
showRaw ? React.createElement("div", { className: "dsh-usage-raw" }, rawJson(d.raw)) : null),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Compact one-line JSON for the raw-payload toggle. */
|
|
1042
|
+
function rawJson(value) {
|
|
1043
|
+
let text;
|
|
1044
|
+
try {
|
|
1045
|
+
text = JSON.stringify(value);
|
|
1046
|
+
} catch {
|
|
1047
|
+
text = String(value);
|
|
1048
|
+
}
|
|
1049
|
+
if (typeof text !== "string") text = String(text);
|
|
1050
|
+
return text.length > 900 ? `${text.slice(0, 900)}…` : text;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** New API route: quota / used / total, normalized by the host. */
|
|
1054
|
+
function NewApiCard({ data, t }) {
|
|
1055
|
+
const d = data.data;
|
|
1056
|
+
const symbol = unitSymbol(d.unit);
|
|
1057
|
+
return React.createElement("div", { className: "dsh-usage-balance-card" },
|
|
1058
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" }, String(d.planName)),
|
|
1059
|
+
row(t("card.balance"), symbol + String(d.remaining), "remaining"),
|
|
1060
|
+
row(t("card.used"), symbol + String(d.used), "used"),
|
|
1061
|
+
row(t("card.total"), symbol + String(d.total), "total"),
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* The model, provider and credential REFERENCE the numbers came from — the
|
|
1067
|
+
* answer to "which key was this queried with", never the key itself.
|
|
1068
|
+
*/
|
|
1069
|
+
function UsageMeta({ data, t }) {
|
|
1070
|
+
const credential = data.credential;
|
|
1071
|
+
const usageUrl = data.data?.usageUrl;
|
|
1072
|
+
return React.createElement("div", { className: "dsh-usage-meta" },
|
|
1073
|
+
React.createElement("div", { className: "dsh-usage-meta-row" },
|
|
1074
|
+
React.createElement("span", { className: "dsh-usage-meta-key" }, t("meta.model")),
|
|
1075
|
+
React.createElement("span", { className: "dsh-usage-meta-val" },
|
|
1076
|
+
`${data.providerName ?? data.provider} / ${data.model}${data.reasoningEffort ? ` · ${data.reasoningEffort}` : ""}`),
|
|
1077
|
+
),
|
|
1078
|
+
credential !== null && credential !== undefined
|
|
1079
|
+
? React.createElement("div", { className: "dsh-usage-meta-row" },
|
|
1080
|
+
React.createElement("span", { className: "dsh-usage-meta-key" }, t("meta.credential")),
|
|
1081
|
+
React.createElement("span", { className: "dsh-usage-meta-val" },
|
|
1082
|
+
`${credential.ref}${credential.source ? ` · ${credential.source}` : ""}`),
|
|
1083
|
+
)
|
|
1084
|
+
: React.createElement("div", { className: "dsh-usage-meta-row" },
|
|
1085
|
+
React.createElement("span", { className: "dsh-usage-meta-key" }, t("meta.credential")),
|
|
1086
|
+
React.createElement("span", { className: "dsh-usage-meta-val" }, t("meta.accountLogin")),
|
|
1087
|
+
),
|
|
1088
|
+
typeof usageUrl === "string" && usageUrl !== ""
|
|
1089
|
+
? React.createElement("div", { className: "dsh-usage-meta-row" },
|
|
1090
|
+
React.createElement("span", { className: "dsh-usage-meta-key" }, t("meta.usageUrl")),
|
|
1091
|
+
React.createElement("span", { className: "dsh-usage-meta-val" }, usageUrl),
|
|
1092
|
+
)
|
|
1093
|
+
: null,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function UsageBalance({ data, history, historyRange, customRange, appliedCustomRange, selectedMonth, onHistoryRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onResetHistory, onRefresh, t }) {
|
|
1098
|
+
let card;
|
|
1099
|
+
if (data.mode === "commandcode") card = React.createElement(CommandCodeCard, { data, t });
|
|
1100
|
+
else if (data.mode === "newapi") card = React.createElement(NewApiCard, { data, t });
|
|
1101
|
+
else if (data.mode === "deepseek-account") card = React.createElement(DeepSeekAccountCard, { data, t });
|
|
1102
|
+
else if (data.mode === "opencode-go" || data.mode === "openrouter"
|
|
1103
|
+
|| data.mode === "moonshot" || data.mode === "siliconflow") {
|
|
1104
|
+
card = React.createElement(UsageRowsCard, { data, t });
|
|
1105
|
+
} else card = React.createElement(DeepSeekCard, { data, t });
|
|
1106
|
+
return React.createElement("div", { className: "dsh-usage-stack" },
|
|
1107
|
+
card,
|
|
1108
|
+
data.mode === "commandcode"
|
|
1109
|
+
? React.createElement(BalanceHistory, {
|
|
1110
|
+
points: history, range: historyRange, onRange: onHistoryRange, t,
|
|
1111
|
+
customRange, appliedCustomRange, selectedMonth, onCustomRange, onApplyCustomRange, onSelectMonth,
|
|
1112
|
+
onClearMonth, onReset: onResetHistory,
|
|
1113
|
+
})
|
|
1114
|
+
: null,
|
|
1115
|
+
React.createElement(UsageMeta, { data, t }),
|
|
1116
|
+
React.createElement("div", { className: "dsh-usage-refresh" },
|
|
1117
|
+
React.createElement(ActionButton, { variant: "ghost", onClick: onRefresh }, t("action.refresh")),
|
|
1118
|
+
),
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Refresh status: how old the numbers on screen are, whether a query is in
|
|
1124
|
+
* flight, and the two controls (auto-refresh toggle, interval settings).
|
|
1125
|
+
* While a refresh runs over existing numbers the age stays visible, so the
|
|
1126
|
+
* panel never blanks out to a spinner.
|
|
1127
|
+
*
|
|
1128
|
+
* The line is deliberately ONE line that never re-flows: the dialog is
|
|
1129
|
+
* vertically centred, so a status text that wrapped while a background
|
|
1130
|
+
* refresh swapped it would move the whole panel under the reader.
|
|
1131
|
+
*/
|
|
1132
|
+
function RefreshStatus({ view, config, now, onToggleAuto, onOpenSettings, t }) {
|
|
1133
|
+
const age = view.updatedAt > 0 ? now - view.updatedAt : null;
|
|
1134
|
+
const label = age === null ? null : formatAge(t, age);
|
|
1135
|
+
const stale = !view.refreshing && age !== null && age > config.intervalMs;
|
|
1136
|
+
let text;
|
|
1137
|
+
if (view.refreshing) {
|
|
1138
|
+
text = label === null
|
|
1139
|
+
? t("status.refreshing")
|
|
1140
|
+
: fill(t("status.refreshingAge"), { age: label });
|
|
1141
|
+
} else if (label === null) text = t("status.never");
|
|
1142
|
+
else {
|
|
1143
|
+
text = fill(t("status.updated"), { age: label })
|
|
1144
|
+
+ (stale ? fill(t("status.stale"), { interval: minutesLabel(t, config.intervalMs) }) : "");
|
|
1145
|
+
}
|
|
1146
|
+
return React.createElement("div", { className: "dsh-usage-status-bar" },
|
|
1147
|
+
React.createElement("div", { className: "dsh-usage-status" },
|
|
1148
|
+
view.refreshing ? React.createElement("span", { className: "dsh-usage-spinner" }) : null,
|
|
1149
|
+
React.createElement("span", { className: "dsh-usage-status-text", title: text }, text),
|
|
1150
|
+
),
|
|
1151
|
+
React.createElement("div", { className: "dsh-usage-status-actions" },
|
|
1152
|
+
React.createElement("button", {
|
|
1153
|
+
type: "button",
|
|
1154
|
+
className: "dsh-usage-chip" + (config.auto ? " dsh-usage-chip-active" : ""),
|
|
1155
|
+
onClick: onToggleAuto,
|
|
1156
|
+
title: config.auto
|
|
1157
|
+
? fill(t("status.autoTooltipOn"), { interval: minutesLabel(t, config.intervalMs) })
|
|
1158
|
+
: t("status.autoTooltipOff"),
|
|
1159
|
+
}, config.auto ? t("status.autoOn") : t("status.autoOff")),
|
|
1160
|
+
React.createElement("button", {
|
|
1161
|
+
type: "button",
|
|
1162
|
+
className: "dsh-usage-chip",
|
|
1163
|
+
onClick: onOpenSettings,
|
|
1164
|
+
}, t("status.settings")),
|
|
1165
|
+
),
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** 刷新设置 form: the auto-refresh switch, preset intervals, and a custom one. */
|
|
1170
|
+
function RefreshSettings({ form, onChange, t }) {
|
|
1171
|
+
const set = (patch) => onChange({ ...form, ...patch });
|
|
1172
|
+
const chip = (label, active, onClick, key) => React.createElement("button", {
|
|
1173
|
+
key: key ?? label,
|
|
1174
|
+
type: "button",
|
|
1175
|
+
className: "dsh-usage-mode-btn" + (active ? " dsh-usage-mode-btn-active" : ""),
|
|
1176
|
+
onClick,
|
|
1177
|
+
}, label);
|
|
1178
|
+
const minutes = Number(form.minutes);
|
|
1179
|
+
const valid = Number.isFinite(minutes) && minutes >= 1 && minutes <= 1440;
|
|
1180
|
+
return React.createElement("div", { className: "dsh-usage-form" },
|
|
1181
|
+
React.createElement("div", { className: "dsh-usage-field" },
|
|
1182
|
+
React.createElement("span", { className: "dsh-usage-field-label" }, t("settings.autoLabel")),
|
|
1183
|
+
React.createElement("div", { className: "dsh-usage-mode-toggle" },
|
|
1184
|
+
chip(t("settings.on"), form.auto === true, () => set({ auto: true })),
|
|
1185
|
+
chip(t("settings.off"), form.auto === false, () => set({ auto: false })),
|
|
1186
|
+
),
|
|
1187
|
+
React.createElement("span", { className: "dsh-usage-hint" }, t("settings.autoHint")),
|
|
1188
|
+
),
|
|
1189
|
+
React.createElement("div", { className: "dsh-usage-field" },
|
|
1190
|
+
React.createElement("span", { className: "dsh-usage-field-label" }, t("settings.interval")),
|
|
1191
|
+
React.createElement("div", { className: "dsh-usage-mode-toggle" },
|
|
1192
|
+
INTERVAL_PRESETS.map((value) => chip(
|
|
1193
|
+
fill(t("minutes.value"), { count: value }),
|
|
1194
|
+
Number(form.minutes) === value,
|
|
1195
|
+
() => set({ minutes: String(value) }),
|
|
1196
|
+
`preset-${value}`,
|
|
1197
|
+
)),
|
|
1198
|
+
),
|
|
1199
|
+
React.createElement("input", {
|
|
1200
|
+
className: "dsh-usage-input",
|
|
1201
|
+
type: "number",
|
|
1202
|
+
min: 1,
|
|
1203
|
+
max: 1440,
|
|
1204
|
+
step: 1,
|
|
1205
|
+
value: form.minutes,
|
|
1206
|
+
"aria-label": t("settings.interval"),
|
|
1207
|
+
onChange: (event) => set({ minutes: event.target.value }),
|
|
1208
|
+
}),
|
|
1209
|
+
React.createElement("span", { className: "dsh-usage-hint" },
|
|
1210
|
+
valid ? t("settings.customHint") : t("settings.customInvalid")),
|
|
1211
|
+
),
|
|
1212
|
+
React.createElement("span", { className: "dsh-usage-hint" }, t("settings.staleHint")),
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Read the current conversation's model selection. The host always passes
|
|
1218
|
+
* `useProjection` for Session slots; the guard keeps the panel alive in a
|
|
1219
|
+
* composition that omits it. The wire view is `{ lastUsed, next }`, where
|
|
1220
|
+
* `next` already resolves a pending pick over the last used route.
|
|
1221
|
+
*/
|
|
1222
|
+
function useModelSelection(props) {
|
|
1223
|
+
const useProjection = props.useProjection;
|
|
1224
|
+
const projected = typeof useProjection === "function" ? useProjection("modelSelection") : undefined;
|
|
1225
|
+
return projected?.next ?? projected?.lastUsed ?? null;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* The 说明 dialog: which provider routes exist and whether each key is set.
|
|
1230
|
+
* A deployment can declare dozens of routes (`llm-pi-ai` ships a whole
|
|
1231
|
+
* catalog), so the ones that matter — routes with a usage API or a
|
|
1232
|
+
* configured key — come first, and the rest wait behind a toggle.
|
|
1233
|
+
*/
|
|
1234
|
+
function ProviderInfo({ providers, t }) {
|
|
1235
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
1236
|
+
if (providers === null) {
|
|
1237
|
+
return React.createElement("div", { className: "dsh-usage-state" }, t("state.loading"));
|
|
1238
|
+
}
|
|
1239
|
+
if (providers.length === 0) {
|
|
1240
|
+
return React.createElement("div", { className: "dsh-usage-state" }, t("providers.none"));
|
|
1241
|
+
}
|
|
1242
|
+
const rank = (entry) => (entry.kind !== null ? 0 : entry.configured ? 1 : 2);
|
|
1243
|
+
const sorted = [...providers].sort((left, right) => rank(left) - rank(right)
|
|
1244
|
+
|| String(left.displayName).localeCompare(String(right.displayName)));
|
|
1245
|
+
const relevant = sorted.filter((entry) => entry.kind !== null || entry.configured === true);
|
|
1246
|
+
const visible = expanded ? sorted : relevant;
|
|
1247
|
+
const hidden = sorted.length - visible.length;
|
|
1248
|
+
const supported = sorted.filter((entry) => entry.kind !== null).length;
|
|
1249
|
+
const configured = sorted.filter((entry) => entry.configured === true).length;
|
|
1250
|
+
return React.createElement("div", { className: "dsh-usage-stack" },
|
|
1251
|
+
React.createElement("div", { className: "dsh-usage-hint" },
|
|
1252
|
+
fill(t("providers.summary"), { total: sorted.length, configured, supported })),
|
|
1253
|
+
React.createElement("div", { className: "dsh-usage-provider-list" },
|
|
1254
|
+
visible.map((entry) => React.createElement("div", {
|
|
1255
|
+
key: entry.provider,
|
|
1256
|
+
className: "dsh-usage-provider-row",
|
|
1257
|
+
},
|
|
1258
|
+
React.createElement("div", { className: "dsh-usage-provider-head" },
|
|
1259
|
+
React.createElement("span", null, String(entry.displayName)),
|
|
1260
|
+
React.createElement("span", { className: "dsh-usage-balance-status" },
|
|
1261
|
+
entry.kind === null
|
|
1262
|
+
? (entry.configured ? t("providers.configuredUnsupported") : t("kind.none"))
|
|
1263
|
+
: (entry.configured ? t("providers.keyConfigured") : t("providers.keyMissing"))),
|
|
1264
|
+
),
|
|
1265
|
+
React.createElement("div", { className: "dsh-usage-provider-meta" },
|
|
1266
|
+
`${entry.provider} · ${kindLabel(t, entry.kind, entry.kindLabel)}${entry.credentialRef ? ` · ${entry.credentialRef}` : ""}`),
|
|
1267
|
+
entry.baseUrl !== null && entry.baseUrl !== undefined
|
|
1268
|
+
? React.createElement("div", { className: "dsh-usage-provider-meta" }, String(entry.baseUrl))
|
|
1269
|
+
: null,
|
|
1270
|
+
))),
|
|
1271
|
+
hidden > 0 || expanded
|
|
1272
|
+
? React.createElement("div", { className: "dsh-usage-provider-toggle" },
|
|
1273
|
+
React.createElement("button", {
|
|
1274
|
+
type: "button",
|
|
1275
|
+
className: "dsh-usage-chip",
|
|
1276
|
+
onClick: () => { setExpanded(!expanded); },
|
|
1277
|
+
}, expanded ? t("providers.collapse") : fill(t("providers.expand"), { count: hidden })))
|
|
1278
|
+
: null,
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* The query-target row: which route the numbers belong to, and the manual
|
|
1284
|
+
* override that can point the panel somewhere other than the conversation.
|
|
1285
|
+
*/
|
|
1286
|
+
function ModelBar({ override, effective, onOpen, t }) {
|
|
1287
|
+
const route = (selection) => override !== null
|
|
1288
|
+
? `${selection.provider}${selection.model ? ` / ${selection.model}` : ""}`
|
|
1289
|
+
: "";
|
|
1290
|
+
const label = override !== null
|
|
1291
|
+
? route(override)
|
|
1292
|
+
: (effective === null
|
|
1293
|
+
? t("modelbar.auto")
|
|
1294
|
+
: fill(t("modelbar.autoRoute"), {
|
|
1295
|
+
route: `${effective.provider}${effective.model ? ` / ${effective.model}` : ""}`,
|
|
1296
|
+
}));
|
|
1297
|
+
return React.createElement("div", { className: "dsh-usage-modelbar" },
|
|
1298
|
+
React.createElement("span", { className: "dsh-usage-modelbar-label" }, t("modelbar.label")),
|
|
1299
|
+
React.createElement("button", {
|
|
1300
|
+
type: "button",
|
|
1301
|
+
className: "dsh-usage-chip" + (override === null ? "" : " dsh-usage-chip-active"),
|
|
1302
|
+
title: override === null ? t("modelbar.titleAuto") : t("modelbar.titleManual"),
|
|
1303
|
+
onClick: onOpen,
|
|
1304
|
+
}, React.createElement("span", { className: "dsh-usage-chip-text" }, label)),
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* The 选择模型 dialog: 自动 (follow the conversation) plus every routable
|
|
1310
|
+
* route, so usage can be checked for a provider the conversation is not on.
|
|
1311
|
+
*/
|
|
1312
|
+
function ModelPicker({ catalog, override, onAuto, onPick, t }) {
|
|
1313
|
+
if (catalog === null) {
|
|
1314
|
+
return React.createElement("div", { className: "dsh-usage-state" }, t("state.loading"));
|
|
1315
|
+
}
|
|
1316
|
+
const groups = Array.isArray(catalog.groups) ? catalog.groups : [];
|
|
1317
|
+
const head = React.createElement("div", { className: "dsh-usage-stack" },
|
|
1318
|
+
React.createElement("button", {
|
|
1319
|
+
type: "button",
|
|
1320
|
+
className: "dsh-usage-mode-btn" + (override === null ? " dsh-usage-mode-btn-active" : ""),
|
|
1321
|
+
onClick: onAuto,
|
|
1322
|
+
}, t("picker.auto")),
|
|
1323
|
+
catalog.default === null || catalog.default === undefined
|
|
1324
|
+
? null
|
|
1325
|
+
: React.createElement("div", { className: "dsh-usage-hint" },
|
|
1326
|
+
fill(t("picker.default"), {
|
|
1327
|
+
route: `${catalog.default.provider}${catalog.default.model ? ` / ${catalog.default.model}` : ""}`,
|
|
1328
|
+
})),
|
|
1329
|
+
);
|
|
1330
|
+
if (groups.length === 0) {
|
|
1331
|
+
return React.createElement("div", { className: "dsh-usage-stack" }, head,
|
|
1332
|
+
React.createElement("div", { className: "dsh-usage-state" }, t("picker.empty")));
|
|
1333
|
+
}
|
|
1334
|
+
return React.createElement("div", { className: "dsh-usage-stack" }, head,
|
|
1335
|
+
groups.map((group) => React.createElement("div", {
|
|
1336
|
+
key: group.provider,
|
|
1337
|
+
className: "dsh-usage-balance-card",
|
|
1338
|
+
},
|
|
1339
|
+
React.createElement("div", { className: "dsh-usage-balance-card-title" },
|
|
1340
|
+
String(group.name),
|
|
1341
|
+
React.createElement("span", { className: "dsh-usage-balance-status" },
|
|
1342
|
+
kindLabel(t, group.kind, group.kindLabel))),
|
|
1343
|
+
React.createElement("div", { className: "dsh-usage-mode-toggle" },
|
|
1344
|
+
group.models.map((model) => React.createElement("button", {
|
|
1345
|
+
key: model.id,
|
|
1346
|
+
type: "button",
|
|
1347
|
+
className: "dsh-usage-mode-btn"
|
|
1348
|
+
+ (override !== null && override.provider === group.provider && override.model === model.id
|
|
1349
|
+
? " dsh-usage-mode-btn-active"
|
|
1350
|
+
: ""),
|
|
1351
|
+
title: model.id,
|
|
1352
|
+
onClick: () => { onPick(group.provider, model.id); },
|
|
1353
|
+
}, React.createElement("span", { className: "dsh-usage-chip-text" }, String(model.name))))),
|
|
1354
|
+
)));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function UsageAction(props) {
|
|
1358
|
+
const { fetchBalance, fetchProviders, fetchModels } = props;
|
|
1359
|
+
// The slot passes the bound translate function; a composition that does
|
|
1360
|
+
// not mount the locale service still renders the Chinese source text
|
|
1361
|
+
// instead of blanking the cell.
|
|
1362
|
+
const t = typeof props.t === "function" ? props.t : (key) => (zh[key] ?? key);
|
|
1363
|
+
const selection = useModelSelection(props);
|
|
1364
|
+
// A manual override outranks the conversation: the panel can inspect any
|
|
1365
|
+
// routable route without switching what the conversation runs on.
|
|
1366
|
+
const [override, setOverride] = React.useState(readModelOverride);
|
|
1367
|
+
const effective = override ?? selection;
|
|
1368
|
+
const provider = effective?.provider ?? null;
|
|
1369
|
+
const model = effective?.model ?? null;
|
|
1370
|
+
const effort = override === null ? (selection?.reasoningEffort ?? null) : null;
|
|
1371
|
+
const sessionId = props.sessionId === undefined ? null : String(props.sessionId);
|
|
1372
|
+
const [open, setOpen] = React.useState(false);
|
|
1373
|
+
const [pickerOpen, setPickerOpen] = React.useState(false);
|
|
1374
|
+
const [catalog, setCatalog] = React.useState(null);
|
|
1375
|
+
const [infoOpen, setInfoOpen] = React.useState(false);
|
|
1376
|
+
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
1377
|
+
const [providers, setProviders] = React.useState(null);
|
|
1378
|
+
const [dialogError, setDialogError] = React.useState("");
|
|
1379
|
+
const [config, setConfig] = React.useState(readRefreshConfig);
|
|
1380
|
+
const [form, setForm] = React.useState({ auto: true, minutes: "5" });
|
|
1381
|
+
const [view, setView] = React.useState({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1382
|
+
const [history, setHistory] = React.useState([]);
|
|
1383
|
+
const [historyRange, setHistoryRange] = React.useState("today");
|
|
1384
|
+
const [customRange, setCustomRange] = React.useState(() => ({
|
|
1385
|
+
start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
|
1386
|
+
end: dateInputValue(Date.now()),
|
|
1387
|
+
}));
|
|
1388
|
+
const [appliedCustomRange, setAppliedCustomRange] = React.useState(() => ({
|
|
1389
|
+
start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
|
1390
|
+
end: dateInputValue(Date.now()),
|
|
1391
|
+
}));
|
|
1392
|
+
const [selectedMonth, setSelectedMonth] = React.useState("");
|
|
1393
|
+
const [, forceRender] = React.useState(0);
|
|
1394
|
+
const viewRef = React.useRef(view);
|
|
1395
|
+
const inFlight = React.useRef(false);
|
|
1396
|
+
const samplePending = React.useRef(false);
|
|
1397
|
+
const generation = React.useRef(0);
|
|
1398
|
+
viewRef.current = view;
|
|
1399
|
+
|
|
1400
|
+
/** One usage query; concurrent calls collapse into the first one. */
|
|
1401
|
+
const refresh = React.useCallback((sample = false) => {
|
|
1402
|
+
if (inFlight.current) {
|
|
1403
|
+
if (sample) samplePending.current = true;
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
inFlight.current = true;
|
|
1407
|
+
const gen = generation.current;
|
|
1408
|
+
// A missing host route binding is reported in the panel, never thrown:
|
|
1409
|
+
// a throwing cell would blank the whole slot entry.
|
|
1410
|
+
if (typeof fetchBalance !== "function") {
|
|
1411
|
+
inFlight.current = false;
|
|
1412
|
+
setView((s) => ({ ...s, refreshing: false, error: t("state.noHost") }));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
setView((s) => ({ ...s, refreshing: true, error: "" }));
|
|
1416
|
+
fetchBalance({ provider, model, effort, sessionId })
|
|
1417
|
+
.then((result) => {
|
|
1418
|
+
// A model switch bumped the generation: this answer is for a route
|
|
1419
|
+
// the panel no longer shows, so it must not write anything.
|
|
1420
|
+
if (gen !== generation.current) return;
|
|
1421
|
+
inFlight.current = false;
|
|
1422
|
+
const pendingSample = samplePending.current;
|
|
1423
|
+
samplePending.current = false;
|
|
1424
|
+
if (result.ok) {
|
|
1425
|
+
const sampledAt = Date.now();
|
|
1426
|
+
const previousHistory = result.value.mode === "commandcode" ? readHistory(result.value) : [];
|
|
1427
|
+
const shouldRecord = sample || (!pendingSample && previousHistory.length === 0);
|
|
1428
|
+
const nextHistory = result.value.mode === "commandcode"
|
|
1429
|
+
? shouldRecord
|
|
1430
|
+
? recordHistorySample(result.value, sampledAt)
|
|
1431
|
+
: previousHistory
|
|
1432
|
+
: [];
|
|
1433
|
+
if (result.value.mode === "commandcode") setHistory(nextHistory);
|
|
1434
|
+
setView((s) => ({ ...s, refreshing: false, data: result.value, error: "", updatedAt: sampledAt }));
|
|
1435
|
+
} else {
|
|
1436
|
+
// Keep the last numbers: a failed background refresh must not
|
|
1437
|
+
// blank out what the user was reading.
|
|
1438
|
+
setView((s) => ({ ...s, refreshing: false, error: result.error.message }));
|
|
1439
|
+
}
|
|
1440
|
+
if (pendingSample && !sample) setTimeout(() => { refresh(true); }, 0);
|
|
1441
|
+
})
|
|
1442
|
+
.catch((err) => {
|
|
1443
|
+
if (gen !== generation.current) return;
|
|
1444
|
+
inFlight.current = false;
|
|
1445
|
+
setView((s) => ({ ...s, refreshing: false, error: String(err) }));
|
|
1446
|
+
if (samplePending.current) {
|
|
1447
|
+
samplePending.current = false;
|
|
1448
|
+
setTimeout(() => { refresh(true); }, 0);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
}, [fetchBalance, provider, model, effort, sessionId]);
|
|
1452
|
+
|
|
1453
|
+
/** Whether what is on screen is older than the configured interval. */
|
|
1454
|
+
const isStale = React.useCallback(() => {
|
|
1455
|
+
const s = viewRef.current;
|
|
1456
|
+
return s.updatedAt === 0 || (Date.now() - s.updatedAt) > config.intervalMs;
|
|
1457
|
+
}, [config.intervalMs]);
|
|
1458
|
+
|
|
1459
|
+
// Mount and every model switch: the new route owns the panel, so drop the
|
|
1460
|
+
// old numbers and query at once — this is what makes the first click show
|
|
1461
|
+
// data instead of starting a query.
|
|
1462
|
+
React.useEffect(() => {
|
|
1463
|
+
generation.current += 1;
|
|
1464
|
+
inFlight.current = false;
|
|
1465
|
+
samplePending.current = false;
|
|
1466
|
+
setView({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1467
|
+
setHistory([]);
|
|
1468
|
+
setSelectedMonth("");
|
|
1469
|
+
refresh();
|
|
1470
|
+
}, [refresh]);
|
|
1471
|
+
|
|
1472
|
+
// Background auto refresh: lives with the cell, not with the modal, so the
|
|
1473
|
+
// numbers stay warm while the panel is closed.
|
|
1474
|
+
React.useEffect(() => {
|
|
1475
|
+
if (!config.auto) return undefined;
|
|
1476
|
+
const id = setInterval(() => { refresh(); }, config.intervalMs);
|
|
1477
|
+
return () => { clearInterval(id); };
|
|
1478
|
+
}, [config.auto, config.intervalMs, refresh]);
|
|
1479
|
+
|
|
1480
|
+
// Sample at the system clock's 00/10/20/30/40/50 minute marks, even when
|
|
1481
|
+
// the configurable display auto-refresh is disabled.
|
|
1482
|
+
React.useEffect(() => {
|
|
1483
|
+
if (view.data?.mode !== "commandcode") return undefined;
|
|
1484
|
+
const now = new Date();
|
|
1485
|
+
const elapsed = (now.getMinutes() % 10) * 60 * 1000 + now.getSeconds() * 1000 + now.getMilliseconds();
|
|
1486
|
+
const wait = HISTORY_INTERVAL_MS - elapsed;
|
|
1487
|
+
let intervalId;
|
|
1488
|
+
const sampleId = setTimeout(() => {
|
|
1489
|
+
refresh(true);
|
|
1490
|
+
intervalId = setInterval(() => { if (!inFlight.current) refresh(true); }, HISTORY_INTERVAL_MS);
|
|
1491
|
+
}, wait);
|
|
1492
|
+
return () => {
|
|
1493
|
+
clearTimeout(sampleId);
|
|
1494
|
+
if (intervalId !== undefined) clearInterval(intervalId);
|
|
1495
|
+
};
|
|
1496
|
+
}, [view.data, refresh]);
|
|
1497
|
+
|
|
1498
|
+
// Opening the panel: show what is known, and only pay for a query when the
|
|
1499
|
+
// numbers are stale (which also renders as 刷新中 over those numbers).
|
|
1500
|
+
React.useEffect(() => {
|
|
1501
|
+
if (!open) return;
|
|
1502
|
+
if (isStale() && !viewRef.current.refreshing) refresh();
|
|
1503
|
+
}, [open, isStale, refresh]);
|
|
1504
|
+
|
|
1505
|
+
// While open: keep the age text moving and self-refresh when the numbers
|
|
1506
|
+
// age past the interval.
|
|
1507
|
+
React.useEffect(() => {
|
|
1508
|
+
if (!open) return undefined;
|
|
1509
|
+
const id = setInterval(() => {
|
|
1510
|
+
forceRender((n) => n + 1);
|
|
1511
|
+
if (isStale() && !viewRef.current.refreshing) refresh();
|
|
1512
|
+
}, TICK_MS);
|
|
1513
|
+
return () => { clearInterval(id); };
|
|
1514
|
+
}, [open, isStale, refresh]);
|
|
1515
|
+
|
|
1516
|
+
const applyConfig = (next) => {
|
|
1517
|
+
setConfig(next);
|
|
1518
|
+
writeRefreshConfig(next);
|
|
1519
|
+
if (next.auto && isStale() && !viewRef.current.refreshing) refresh();
|
|
1520
|
+
};
|
|
1521
|
+
const toggleAuto = () => { applyConfig({ auto: !config.auto, intervalMs: config.intervalMs }); };
|
|
1522
|
+
// One dialog at a time: a sub-dialog replaces the panel, and closing it
|
|
1523
|
+
// returns to the panel. Nothing is stacked, so a background refresh
|
|
1524
|
+
// re-rendering the cell cannot reflow or re-order two centred cards
|
|
1525
|
+
// while the user is reading one of them.
|
|
1526
|
+
const openSettings = () => {
|
|
1527
|
+
setForm({ auto: config.auto, minutes: String(Math.max(1, Math.round(config.intervalMs / 60000))) });
|
|
1528
|
+
setOpen(false);
|
|
1529
|
+
setSettingsOpen(true);
|
|
1530
|
+
};
|
|
1531
|
+
const closeSettings = () => {
|
|
1532
|
+
setSettingsOpen(false);
|
|
1533
|
+
setOpen(true);
|
|
1534
|
+
};
|
|
1535
|
+
const saveSettings = () => {
|
|
1536
|
+
const minutes = Number(form.minutes);
|
|
1537
|
+
const intervalMs = Number.isFinite(minutes) && minutes >= 1
|
|
1538
|
+
? clampInterval(minutes * 60000)
|
|
1539
|
+
: config.intervalMs;
|
|
1540
|
+
setSettingsOpen(false);
|
|
1541
|
+
setOpen(true);
|
|
1542
|
+
applyConfig({ auto: form.auto === true, intervalMs });
|
|
1543
|
+
};
|
|
1544
|
+
const openInfo = () => {
|
|
1545
|
+
setOpen(false);
|
|
1546
|
+
setProviders(null);
|
|
1547
|
+
setDialogError("");
|
|
1548
|
+
setInfoOpen(true);
|
|
1549
|
+
fetchProviders()
|
|
1550
|
+
.then((result) => setProviders(result.ok ? result.value : []))
|
|
1551
|
+
.catch((err) => { setDialogError(String(err)); setProviders([]); });
|
|
1552
|
+
};
|
|
1553
|
+
const closeInfo = () => {
|
|
1554
|
+
setInfoOpen(false);
|
|
1555
|
+
setOpen(true);
|
|
1556
|
+
};
|
|
1557
|
+
const openPicker = () => {
|
|
1558
|
+
setOpen(false);
|
|
1559
|
+
setCatalog(null);
|
|
1560
|
+
setDialogError("");
|
|
1561
|
+
setPickerOpen(true);
|
|
1562
|
+
fetchModels()
|
|
1563
|
+
.then((result) => setCatalog(result.ok ? result.value : { groups: [], default: null }))
|
|
1564
|
+
.catch((err) => { setDialogError(String(err)); setCatalog({ groups: [], default: null }); });
|
|
1565
|
+
};
|
|
1566
|
+
const closePicker = () => {
|
|
1567
|
+
setPickerOpen(false);
|
|
1568
|
+
setOpen(true);
|
|
1569
|
+
};
|
|
1570
|
+
// Switching the target resets the route: the effect below re-queries at once.
|
|
1571
|
+
const pickModel = (nextProvider, nextModel) => {
|
|
1572
|
+
const next = { provider: nextProvider, model: nextModel };
|
|
1573
|
+
setOverride(next);
|
|
1574
|
+
writeModelOverride(next);
|
|
1575
|
+
setPickerOpen(false);
|
|
1576
|
+
setOpen(true);
|
|
1577
|
+
};
|
|
1578
|
+
const pickAuto = () => {
|
|
1579
|
+
setOverride(null);
|
|
1580
|
+
writeModelOverride(null);
|
|
1581
|
+
setPickerOpen(false);
|
|
1582
|
+
setOpen(true);
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
const clearMonth = (month) => {
|
|
1586
|
+
if (view.data?.mode !== "commandcode" || month === "") return;
|
|
1587
|
+
const message = fill(t("history.confirmClearMonth"), { month });
|
|
1588
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(message)) return;
|
|
1589
|
+
const next = clearHistoryMonth(view.data, month);
|
|
1590
|
+
setHistory(next);
|
|
1591
|
+
setSelectedMonth(storedMonths(next)[0] ?? "");
|
|
1592
|
+
};
|
|
1593
|
+
const resetHistory = () => {
|
|
1594
|
+
if (view.data?.mode !== "commandcode") return;
|
|
1595
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(t("history.confirmReset"))) return;
|
|
1596
|
+
setHistory(resetHistoryStorage(view.data));
|
|
1597
|
+
setSelectedMonth("");
|
|
1598
|
+
};
|
|
1599
|
+
const applyCustomRange = () => {
|
|
1600
|
+
const start = localDateStart(customRange.start);
|
|
1601
|
+
const end = localDateStart(customRange.end);
|
|
1602
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
|
|
1603
|
+
setAppliedCustomRange({ ...customRange });
|
|
1604
|
+
setHistoryRange("custom");
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
let body;
|
|
1608
|
+
if (view.data !== null) {
|
|
1609
|
+
body = React.createElement("div", { className: "dsh-usage-stack" },
|
|
1610
|
+
React.createElement(UsageBalance, {
|
|
1611
|
+
data: view.data,
|
|
1612
|
+
history,
|
|
1613
|
+
historyRange,
|
|
1614
|
+
customRange,
|
|
1615
|
+
appliedCustomRange,
|
|
1616
|
+
selectedMonth,
|
|
1617
|
+
onHistoryRange: setHistoryRange,
|
|
1618
|
+
onCustomRange: setCustomRange,
|
|
1619
|
+
onApplyCustomRange: applyCustomRange,
|
|
1620
|
+
onSelectMonth: setSelectedMonth,
|
|
1621
|
+
onClearMonth: clearMonth,
|
|
1622
|
+
onResetHistory: resetHistory,
|
|
1623
|
+
onRefresh: refresh,
|
|
1624
|
+
t,
|
|
1625
|
+
}),
|
|
1626
|
+
view.error !== ""
|
|
1627
|
+
? React.createElement("div", { className: "dsh-usage-inline-error" },
|
|
1628
|
+
fill(t("error.lastRefresh"), { error: view.error }))
|
|
1629
|
+
: null,
|
|
1630
|
+
);
|
|
1631
|
+
} else if (view.refreshing) {
|
|
1632
|
+
body = React.createElement("div", { className: "dsh-usage-state" }, t("state.querying"));
|
|
1633
|
+
} else if (view.error !== "") {
|
|
1634
|
+
body = React.createElement("div", { className: "dsh-usage-error" },
|
|
1635
|
+
React.createElement("div", { className: "dsh-usage-error-text" }, view.error),
|
|
1636
|
+
React.createElement(ActionButton, { variant: "outline", onClick: refresh }, t("action.retry")),
|
|
1637
|
+
);
|
|
1638
|
+
} else {
|
|
1639
|
+
body = React.createElement("div", { className: "dsh-usage-state" }, t("state.empty"));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const now = Date.now();
|
|
1643
|
+
// Deliberately stable: a title that changed on every background refresh
|
|
1644
|
+
// would re-open its tooltip under a resting pointer.
|
|
1645
|
+
const buttonTitle = config.auto
|
|
1646
|
+
? fill(t("button.titleAuto"), { interval: minutesLabel(t, config.intervalMs) })
|
|
1647
|
+
: t("button.titleManual");
|
|
1648
|
+
|
|
1649
|
+
return React.createElement(React.Fragment, null,
|
|
1650
|
+
React.createElement("style", {
|
|
1651
|
+
"data-plugin-css": "dsh-model-usage/styles",
|
|
1652
|
+
dangerouslySetInnerHTML: { __html: STYLES },
|
|
1653
|
+
}),
|
|
1654
|
+
React.createElement("button", {
|
|
1655
|
+
type: "button",
|
|
1656
|
+
className: "dsh-usage-btn" + (view.refreshing ? " dsh-usage-btn-busy" : ""),
|
|
1657
|
+
title: buttonTitle,
|
|
1658
|
+
onClick: () => { setOpen(true); },
|
|
1659
|
+
}, t("button.label")),
|
|
1660
|
+
React.createElement(UsageModal, {
|
|
1661
|
+
open,
|
|
1662
|
+
onClose: () => { setOpen(false); },
|
|
1663
|
+
title: t("button.label"),
|
|
1664
|
+
closeLabel: t("action.close"),
|
|
1665
|
+
footer: React.createElement(React.Fragment, null,
|
|
1666
|
+
React.createElement(ActionButton, { variant: "ghost", onClick: openInfo }, t("action.info")),
|
|
1667
|
+
React.createElement(ActionButton, { variant: "primary", onClick: () => { setOpen(false); } }, t("action.close")),
|
|
1668
|
+
),
|
|
1669
|
+
}, React.createElement("div", { className: "dsh-usage-modal-body" },
|
|
1670
|
+
React.createElement("div", { className: "dsh-usage-stack" },
|
|
1671
|
+
React.createElement(RefreshStatus, {
|
|
1672
|
+
view,
|
|
1673
|
+
config,
|
|
1674
|
+
now,
|
|
1675
|
+
onToggleAuto: toggleAuto,
|
|
1676
|
+
onOpenSettings: openSettings,
|
|
1677
|
+
t,
|
|
1678
|
+
}),
|
|
1679
|
+
React.createElement(ModelBar, { override, effective, onOpen: openPicker, t }),
|
|
1680
|
+
body,
|
|
1681
|
+
))),
|
|
1682
|
+
React.createElement(UsageModal, {
|
|
1683
|
+
open: pickerOpen,
|
|
1684
|
+
onClose: closePicker,
|
|
1685
|
+
title: t("picker.title"),
|
|
1686
|
+
closeLabel: t("action.cancel"),
|
|
1687
|
+
footer: React.createElement(React.Fragment, null,
|
|
1688
|
+
React.createElement(ActionButton, { variant: "outline", onClick: closePicker }, t("action.cancel")),
|
|
1689
|
+
React.createElement(ActionButton, { variant: "ghost", onClick: pickAuto }, t("action.auto")),
|
|
1690
|
+
),
|
|
1691
|
+
}, React.createElement("div", { className: "dsh-usage-modal-body" },
|
|
1692
|
+
React.createElement("div", { className: "dsh-usage-stack" },
|
|
1693
|
+
React.createElement("div", { className: "dsh-usage-hint" }, t("picker.hint")),
|
|
1694
|
+
dialogError !== "" ? React.createElement("div", { className: "dsh-usage-inline-error" }, dialogError) : null,
|
|
1695
|
+
React.createElement(ModelPicker, { catalog, override, onAuto: pickAuto, onPick: pickModel, t }),
|
|
1696
|
+
))),
|
|
1697
|
+
React.createElement(UsageModal, {
|
|
1698
|
+
open: settingsOpen,
|
|
1699
|
+
onClose: closeSettings,
|
|
1700
|
+
title: t("settings.title"),
|
|
1701
|
+
closeLabel: t("action.cancel"),
|
|
1702
|
+
footer: React.createElement(React.Fragment, null,
|
|
1703
|
+
React.createElement(ActionButton, { variant: "outline", onClick: closeSettings }, t("action.cancel")),
|
|
1704
|
+
React.createElement(ActionButton, { variant: "primary", onClick: saveSettings }, t("action.save")),
|
|
1705
|
+
),
|
|
1706
|
+
}, React.createElement("div", { className: "dsh-usage-modal-body" },
|
|
1707
|
+
React.createElement(RefreshSettings, { form, onChange: setForm, t }))),
|
|
1708
|
+
React.createElement(UsageModal, {
|
|
1709
|
+
open: infoOpen,
|
|
1710
|
+
onClose: closeInfo,
|
|
1711
|
+
title: t("info.title"),
|
|
1712
|
+
closeLabel: t("action.close"),
|
|
1713
|
+
footer: React.createElement(ActionButton, { variant: "primary", onClick: closeInfo }, t("action.close")),
|
|
1714
|
+
}, React.createElement("div", { className: "dsh-usage-modal-body" },
|
|
1715
|
+
React.createElement("div", { className: "dsh-usage-stack" },
|
|
1716
|
+
React.createElement("div", { className: "dsh-usage-hint" }, t("info.hint")),
|
|
1717
|
+
dialogError !== "" ? React.createElement("div", { className: "dsh-usage-inline-error" }, dialogError) : null,
|
|
1718
|
+
React.createElement(ProviderInfo, { providers, t }),
|
|
1719
|
+
))),
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function apply(ctx) {
|
|
1724
|
+
// Copy lives in the Client locale service, so a language switch re-renders
|
|
1725
|
+
// this cell. The service is read through `ctx.get()` rather than declared
|
|
1726
|
+
// as a hard dependency: a composition that does not mount it still gets
|
|
1727
|
+
// the panel (in the Chinese source language) instead of no panel at all.
|
|
1728
|
+
const locale = typeof ctx.get === "function" ? ctx.get("locale") : undefined;
|
|
1729
|
+
let t = (key) => (zh[key] ?? key);
|
|
1730
|
+
if (locale !== undefined && typeof locale.register === "function" && typeof locale.bind === "function") {
|
|
1731
|
+
ctx.effect(() => locale.register(NS, { zh, en }), "model-usage: dictionaries");
|
|
1732
|
+
t = locale.bind(NS);
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* One JSON read from a host route. A refused request (the trust fence
|
|
1736
|
+
* answers 401/403) becomes a readable error instead of a JSON parse
|
|
1737
|
+
* failure, so the panel says what happened.
|
|
1738
|
+
*/
|
|
1739
|
+
const readEnvelope = (res) => {
|
|
1740
|
+
if (res.ok) return res.json();
|
|
1741
|
+
throw new Error(`HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`);
|
|
1742
|
+
};
|
|
1743
|
+
/** Ask the host for the usage of one conversation model (secrets stay host-side). */
|
|
1744
|
+
const fetchBalance = (selection) => {
|
|
1745
|
+
const payload = {};
|
|
1746
|
+
if (selection?.provider) payload.provider = selection.provider;
|
|
1747
|
+
if (selection?.model) payload.model = selection.model;
|
|
1748
|
+
if (selection?.effort) payload.reasoningEffort = selection.effort;
|
|
1749
|
+
if (selection?.sessionId) payload.sessionId = selection.sessionId;
|
|
1750
|
+
// The host itself gives up after 30s; this is the outer guard that makes
|
|
1751
|
+
// sure a stranded request cannot keep the single in-flight slot busy and
|
|
1752
|
+
// silently stop every later auto refresh.
|
|
1753
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
1754
|
+
const timer = controller === null ? null : setTimeout(() => { controller.abort(); }, 45000);
|
|
1755
|
+
return fetch(ROUTE_BALANCE, {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
headers: { "content-type": "application/json" },
|
|
1758
|
+
body: JSON.stringify(payload),
|
|
1759
|
+
...(controller === null ? {} : { signal: controller.signal }),
|
|
1760
|
+
})
|
|
1761
|
+
.then(readEnvelope)
|
|
1762
|
+
.finally(() => { if (timer !== null) clearTimeout(timer); });
|
|
1763
|
+
};
|
|
1764
|
+
/** Read-only provider/credential directory (reference names only). */
|
|
1765
|
+
const fetchProviders = () => fetch(ROUTE_PROVIDERS).then(readEnvelope);
|
|
1766
|
+
/** Routable models + per-route usage kind, for the manual picker. */
|
|
1767
|
+
const fetchModels = () => fetch(ROUTE_MODELS).then(readEnvelope);
|
|
1768
|
+
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
1769
|
+
name: "conversation.session.header.utilities",
|
|
1770
|
+
id: "model-usage",
|
|
1771
|
+
order: 100,
|
|
1772
|
+
locale: NS,
|
|
1773
|
+
label: () => t("button.label"),
|
|
1774
|
+
inject: () => ({ fetchBalance, fetchProviders, fetchModels, t }),
|
|
1775
|
+
}, UsageAction));
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// `slots` is the one hard dependency (the cell is the whole point); every
|
|
1779
|
+
// other service, locale included, is read through `ctx.get()` so the plugin
|
|
1780
|
+
// mounts in as many compositions as possible.
|
|
1781
|
+
const inject = ["slots"];
|
|
1782
|
+
|
|
1783
|
+
exports.apply = apply;
|
|
1784
|
+
exports.inject = inject;
|
|
1785
|
+
return module.exports;
|
|
1786
|
+
},
|
|
1787
|
+
});
|