dsh-model-usage 0.8.3 → 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 +6 -0
- package/README.md +20 -10
- 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 +28 -6
- package/lib/client.js +284 -105
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/docs/screenshots/usage-panel.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented in this file. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.8.4
|
|
6
|
+
|
|
7
|
+
- Command Code balance history: sample on ten-minute clock marks, retain the current and two
|
|
8
|
+
preceding calendar months, support custom date ranges and daily week/month aggregation, and add
|
|
9
|
+
month deletion and full reset controls.
|
|
10
|
+
|
|
5
11
|
## 0.8.3
|
|
6
12
|
|
|
7
13
|
- Command Code balance history: record account balance snapshots every 30 minutes, retain up to
|
package/README.md
CHANGED
|
@@ -8,13 +8,20 @@ using.
|
|
|
8
8
|
Queries run in the host process with the provider's own credential reference. The browser receives
|
|
9
9
|
normalized values and the credential reference name; credential values never leave the host.
|
|
10
10
|
|
|
11
|
-

|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+

|
|
12
20
|
|
|
13
21
|
*Usage panel for a Command Code subscription (Chinese UI): five-hour and weekly quota windows,
|
|
14
22
|
credit balance, spend, request count, and the model and credential reference used for the query.*
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
模型与凭据引用。*
|
|
24
|
+
*用量面板:兼容深浅色模式*
|
|
18
25
|
|
|
19
26
|
Published on npm as [`dsh-model-usage`](https://www.npmjs.com/package/dsh-model-usage).
|
|
20
27
|
|
|
@@ -26,10 +33,11 @@ Published on npm as [`dsh-model-usage`](https://www.npmjs.com/package/dsh-model-
|
|
|
26
33
|
model the conversation runs on.
|
|
27
34
|
- **Background refresh.** The mounted component refreshes on an interval, including while the panel
|
|
28
35
|
is closed.
|
|
29
|
-
- **Command Code balance history.**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
- **Command Code balance history.** The plugin queries on the system clock's ten-minute marks
|
|
37
|
+
(`:00`, `:10`, `:20`, …), independently of the display refresh setting. Samples are kept locally
|
|
38
|
+
per account for the current calendar month and the two preceding calendar months. The chart has
|
|
39
|
+
uniform time-axis ticks, actual sample positions for short ranges, daily totals for week/month
|
|
40
|
+
views, custom start/end dates, and month deletion/reset controls.
|
|
33
41
|
- **Provider directory.** The deployment's provider routes are listed with their usage adapter,
|
|
34
42
|
credential reference name and configuration state. Values are never read out.
|
|
35
43
|
|
|
@@ -254,9 +262,11 @@ OpenCode Zen(按量付费)没有用量接口(`/zen/v1/usage`、`/me`、`/c
|
|
|
254
262
|
5 分钟。策略保存在 `dsh-model-usage:refresh`。
|
|
255
263
|
- 数据仍在间隔内时打开面板不会发起请求;「刷新」按钮可强制查询。
|
|
256
264
|
- 查询失败时保留上次数据并追加 `上次刷新失败:…`;仅在从未成功时显示整屏错误与「重试」。
|
|
257
|
-
- Command Code
|
|
258
|
-
|
|
259
|
-
|
|
265
|
+
- Command Code 余额历史独立于面板刷新开关,在系统时间的每小时 `00 / 10 / 20 / 30 / 40 / 50`
|
|
266
|
+
分自动查询。数据按账户保存在浏览器本地,保留当前自然月及前两个月;例如十月会保留十月、
|
|
267
|
+
九月、八月,七月数据自动清除。用量卡片提供「今天 / 昨天 / 近一周 / 近一月 / 自定义」筛选,
|
|
268
|
+
周和月按每天合计,短时间段按真实查询时间定位折线点,并可清除指定月份或重置当前账户记录。
|
|
269
|
+
首次记录作为余额基线,后续每段消耗按前后余额差计算,余额增加时该段显示为零。
|
|
260
270
|
- host 侧单次用量请求超时 30 秒,浏览器侧另有 45 秒外层超时。
|
|
261
271
|
|
|
262
272
|
### 安全
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/icon.svg
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Usage">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="usage-bg" x1="7" y1="4" x2="57" y2="60" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#0A84FF"/>
|
|
5
|
+
<stop offset="0.55" stop-color="#147EFB"/>
|
|
6
|
+
<stop offset="1" stop-color="#30D158"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="usage-glass" x1="13" y1="11" x2="51" y2="55" gradientUnits="userSpaceOnUse">
|
|
9
|
+
<stop stop-color="#FFFFFF" stop-opacity="0.24"/>
|
|
10
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.08"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<filter id="usage-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
13
|
+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#003C7A" flood-opacity="0.22"/>
|
|
14
|
+
</filter>
|
|
15
|
+
</defs>
|
|
16
|
+
|
|
17
|
+
<rect x="2" y="2" width="60" height="60" rx="17" fill="url(#usage-bg)"/>
|
|
18
|
+
<rect x="8" y="8" width="48" height="48" rx="13" fill="url(#usage-glass)" stroke="#FFFFFF" stroke-opacity="0.22"/>
|
|
19
|
+
|
|
20
|
+
<g filter="url(#usage-shadow)" fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<path d="M16 45.5h32" stroke-width="2" stroke-opacity="0.42"/>
|
|
22
|
+
<path d="M16 40.5 25 33l7 5 13-15" stroke-width="4"/>
|
|
23
|
+
<path d="m40.5 23 4.5 0 0 4.5" stroke-width="3"/>
|
|
24
|
+
</g>
|
|
25
|
+
|
|
26
|
+
<circle cx="16" cy="40.5" r="3.5" fill="#FFFFFF"/>
|
|
27
|
+
<circle cx="45" cy="23" r="3.5" fill="#30D158" stroke="#FFFFFF" stroke-width="2"/>
|
|
28
|
+
<circle cx="45" cy="23" r="1.2" fill="#FFFFFF"/>
|
|
7
29
|
</svg>
|
package/lib/client.js
CHANGED
|
@@ -122,12 +122,22 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
"card.hideRaw": "隐藏原始返回",
|
|
123
123
|
"card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
|
|
124
124
|
"history.title": "余额消耗趋势",
|
|
125
|
-
"history.range.30m": "最近半小时",
|
|
126
|
-
"history.range.12h": "最近半天",
|
|
127
125
|
"history.range.today": "今天",
|
|
128
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": "应用时间段",
|
|
129
133
|
"history.empty": "该时间段暂无足够的余额记录",
|
|
130
134
|
"history.amount": "消耗",
|
|
135
|
+
"history.clearMonth": "清除所选月份",
|
|
136
|
+
"history.confirmClearMonth": "确定清除 {month} 的余额记录吗?",
|
|
137
|
+
"history.reset": "重置当前记录",
|
|
138
|
+
"history.month": "数据月份",
|
|
139
|
+
"history.noMonths": "暂无可清理的月份",
|
|
140
|
+
"history.confirmReset": "确定清除当前账户的全部余额记录吗?",
|
|
131
141
|
"card.used": "已用",
|
|
132
142
|
"card.total": "总额",
|
|
133
143
|
"window.fiveHour": "5 小时窗口",
|
|
@@ -230,12 +240,22 @@ window.__ModuleLoader__.load({
|
|
|
230
240
|
"card.hideRaw": "Hide raw response",
|
|
231
241
|
"card.unrecognized": "These fields are not recognized yet — the raw response is below (send it over to calibrate)",
|
|
232
242
|
"history.title": "Balance spend trend",
|
|
233
|
-
"history.range.30m": "Last 30 min",
|
|
234
|
-
"history.range.12h": "Last 12 hours",
|
|
235
243
|
"history.range.today": "Today",
|
|
236
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",
|
|
237
251
|
"history.empty": "Not enough balance samples for this period",
|
|
238
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?",
|
|
239
259
|
"card.used": "Used",
|
|
240
260
|
"card.total": "Total",
|
|
241
261
|
"window.fiveHour": "5-hour window",
|
|
@@ -291,47 +311,49 @@ window.__ModuleLoader__.load({
|
|
|
291
311
|
const STYLES = [
|
|
292
312
|
"@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }",
|
|
293
313
|
"@keyframes dsh-usage-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }",
|
|
294
|
-
".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:
|
|
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; }",
|
|
295
315
|
".dsh-usage-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
316
|
+
".dsh-usage-btn:active { transform: scale(0.98); }",
|
|
296
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; }",
|
|
297
|
-
".dsh-usage-action { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0
|
|
298
|
-
".dsh-usage-action
|
|
299
|
-
".dsh-usage-action-
|
|
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); }",
|
|
300
322
|
".dsh-usage-action-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }",
|
|
301
323
|
".dsh-usage-action-outline:hover, .dsh-usage-action-ghost:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
302
324
|
".dsh-usage-action-ghost:hover { color: var(--dsw-alias-label-primary); }",
|
|
303
|
-
".dsh-usage-modal-overlay { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding:
|
|
304
|
-
".dsh-usage-modal-dialog { display: flex; flex-direction: column; width: min(
|
|
305
|
-
".dsh-usage-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding:
|
|
306
|
-
".dsh-usage-modal-title { font-size:
|
|
307
|
-
".dsh-usage-modal-close { display: inline-flex; align-items: center; justify-content: center; width:
|
|
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; }",
|
|
308
330
|
".dsh-usage-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
|
|
309
|
-
".dsh-usage-modal-content { padding:
|
|
310
|
-
".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px
|
|
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); }",
|
|
311
333
|
".dsh-usage-state { padding: 12px 0; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); }",
|
|
312
334
|
".dsh-usage-error { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; padding: 4px 0; }",
|
|
313
335
|
".dsh-usage-error-text { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); word-break: break-all; }",
|
|
314
336
|
".dsh-usage-inline-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); word-break: break-all; }",
|
|
315
|
-
".dsh-usage-stack { display: flex; flex-direction: column; gap:
|
|
337
|
+
".dsh-usage-stack { display: flex; flex-direction: column; gap: 12px; }",
|
|
316
338
|
".dsh-usage-status-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
317
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; }",
|
|
318
340
|
".dsh-usage-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
|
|
319
341
|
".dsh-usage-status-actions { display: inline-flex; align-items: center; gap: 6px; }",
|
|
320
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; }",
|
|
321
|
-
".dsh-usage-chip { display: inline-flex; align-items: center; height:
|
|
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; }",
|
|
322
344
|
".dsh-usage-chip:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
323
|
-
".dsh-usage-chip-active { color:
|
|
345
|
+
".dsh-usage-chip-active { color: #FFFFFF; border-color: #0A84FF; background: #0A84FF; }",
|
|
324
346
|
".dsh-usage-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 230px; }",
|
|
325
347
|
".dsh-usage-modelbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }",
|
|
326
348
|
".dsh-usage-modelbar-label { flex: 0 0 auto; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
327
|
-
".dsh-usage-balance-card { border: 1px solid var(--dsw-alias-border-l2); border-radius:
|
|
328
|
-
".dsh-usage-balance-card-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight:
|
|
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); }",
|
|
329
351
|
".dsh-usage-balance-status { font-size: 12px; font-weight: 400; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
330
|
-
".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }",
|
|
352
|
+
".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 25px; }",
|
|
331
353
|
".dsh-usage-balance-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
332
|
-
".dsh-usage-balance-value { font-size:
|
|
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; }",
|
|
333
355
|
".dsh-usage-refresh { display: flex; justify-content: flex-end; }",
|
|
334
|
-
".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top:
|
|
356
|
+
".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 12px; display: flex; flex-direction: column; gap: 6px; }",
|
|
335
357
|
".dsh-usage-meta-row { display: flex; gap: 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
336
358
|
".dsh-usage-meta-key { flex: 0 0 auto; }",
|
|
337
359
|
".dsh-usage-meta-val { flex: 1 1 auto; word-break: break-all; font-variant-numeric: tabular-nums; }",
|
|
@@ -339,11 +361,11 @@ window.__ModuleLoader__.load({
|
|
|
339
361
|
".dsh-usage-field { display: flex; flex-direction: column; gap: 6px; }",
|
|
340
362
|
".dsh-usage-field-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
341
363
|
".dsh-usage-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
342
|
-
".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-size: 13px; line-height: 20px; }",
|
|
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; }",
|
|
343
365
|
".dsh-usage-mode-toggle { display: flex; flex-wrap: wrap; gap: 8px; }",
|
|
344
|
-
".dsh-usage-mode-btn { display: inline-flex; align-items: center; justify-content: center; height: 30px; padding: 0 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background:
|
|
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; }",
|
|
345
367
|
".dsh-usage-mode-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
346
|
-
".dsh-usage-mode-btn-active { background:
|
|
368
|
+
".dsh-usage-mode-btn-active { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; }",
|
|
347
369
|
".dsh-usage-cc-window { display: flex; flex-direction: column; gap: 6px; }",
|
|
348
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); }",
|
|
349
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; }",
|
|
@@ -355,11 +377,18 @@ window.__ModuleLoader__.load({
|
|
|
355
377
|
".dsh-usage-history { display: flex; flex-direction: column; gap: 10px; }",
|
|
356
378
|
".dsh-usage-history-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
357
379
|
".dsh-usage-history-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }",
|
|
358
|
-
".dsh-usage-history-ranges { display: flex; gap:
|
|
359
|
-
".dsh-usage-history-ranges .dsh-usage-mode-btn { height:
|
|
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; }",
|
|
360
389
|
".dsh-usage-history-chart { width: 100%; height: 148px; overflow: visible; }",
|
|
361
390
|
".dsh-usage-history-empty { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 8px 0; }",
|
|
362
|
-
".dsh-usage-modal-body { max-height: max(140px, calc(100vh -
|
|
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; }",
|
|
363
392
|
".dsh-usage-provider-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }",
|
|
364
393
|
".dsh-usage-provider-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; }",
|
|
365
394
|
".dsh-usage-provider-row + .dsh-usage-provider-row { border-top: 1px solid var(--dsw-alias-border-l2); }",
|
|
@@ -380,9 +409,8 @@ window.__ModuleLoader__.load({
|
|
|
380
409
|
/** How often the open panel re-reads the clock and re-checks staleness. */
|
|
381
410
|
const TICK_MS = 5000;
|
|
382
411
|
const HISTORY_KEY = "dsh-model-usage:commandcode-history";
|
|
383
|
-
const HISTORY_INTERVAL_MS =
|
|
384
|
-
const
|
|
385
|
-
const HISTORY_RANGES = ["30m", "12h", "today", "yesterday"];
|
|
412
|
+
const HISTORY_INTERVAL_MS = 10 * 60 * 1000;
|
|
413
|
+
const HISTORY_RANGES = ["today", "yesterday", "week", "month", "custom"];
|
|
386
414
|
/** Everything Tab may land on inside a dialog, for the focus trap. */
|
|
387
415
|
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
388
416
|
|
|
@@ -428,6 +456,13 @@ window.__ModuleLoader__.load({
|
|
|
428
456
|
return `${HISTORY_KEY}:${encodeURIComponent(account)}`;
|
|
429
457
|
}
|
|
430
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
|
+
|
|
431
466
|
function readHistory(data) {
|
|
432
467
|
const key = historyStorageKey(data);
|
|
433
468
|
if (key === null) return [];
|
|
@@ -435,7 +470,7 @@ window.__ModuleLoader__.load({
|
|
|
435
470
|
const parsed = JSON.parse(localStorage.getItem(key) ?? "[]");
|
|
436
471
|
if (!Array.isArray(parsed)) return [];
|
|
437
472
|
const now = Date.now();
|
|
438
|
-
const cutoff = now
|
|
473
|
+
const cutoff = historyCutoff(now);
|
|
439
474
|
const retained = parsed.filter((point) => point !== null && typeof point === "object"
|
|
440
475
|
&& Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
|
|
441
476
|
&& Number.isFinite(point.balance)
|
|
@@ -454,19 +489,43 @@ window.__ModuleLoader__.load({
|
|
|
454
489
|
}
|
|
455
490
|
}
|
|
456
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
|
+
|
|
457
515
|
function recordHistorySample(data, now = Date.now()) {
|
|
458
516
|
const key = historyStorageKey(data);
|
|
459
517
|
const balance = Number(data?.data?.credits?.totalRemaining);
|
|
460
518
|
if (key === null || !Number.isFinite(balance)) return [];
|
|
519
|
+
pruneStoredHistories(now);
|
|
461
520
|
const points = readHistory(data);
|
|
462
521
|
const last = points[points.length - 1];
|
|
463
|
-
if (last !== undefined &&
|
|
522
|
+
if (last !== undefined && Math.floor(last.at / HISTORY_INTERVAL_MS) === Math.floor(now / HISTORY_INTERVAL_MS)) return points;
|
|
464
523
|
points.push({
|
|
465
524
|
at: now,
|
|
466
525
|
balance,
|
|
467
526
|
spend: last === undefined ? null : Math.max(0, last.balance - balance),
|
|
468
527
|
});
|
|
469
|
-
const retained = points.filter((point) => point.at >= now
|
|
528
|
+
const retained = points.filter((point) => point.at >= historyCutoff(now));
|
|
470
529
|
try {
|
|
471
530
|
localStorage.setItem(key, JSON.stringify(retained));
|
|
472
531
|
} catch {
|
|
@@ -475,6 +534,33 @@ window.__ModuleLoader__.load({
|
|
|
475
534
|
return retained;
|
|
476
535
|
}
|
|
477
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
|
+
|
|
478
564
|
const MODEL_KEY = "dsh-model-usage:model";
|
|
479
565
|
/** Key this plugin used before it was renamed to dsh-model-usage. */
|
|
480
566
|
const LEGACY_MODEL_KEY = "dsh-desktop-usage:model";
|
|
@@ -737,82 +823,118 @@ window.__ModuleLoader__.load({
|
|
|
737
823
|
);
|
|
738
824
|
}
|
|
739
825
|
|
|
740
|
-
function
|
|
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 }) {
|
|
741
839
|
const now = Date.now();
|
|
742
|
-
|
|
743
|
-
let
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
start
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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);
|
|
755
872
|
}
|
|
756
|
-
const visible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
|
|
757
873
|
const maxSpend = Math.max(0.01, ...visible.map((point) => point.spend));
|
|
758
874
|
const chartPoints = visible.map((point) => ({
|
|
759
|
-
x: 42 + ((point.at - start) / (end - start)) * 306,
|
|
875
|
+
x: 42 + Math.max(0, Math.min(1, (point.at - start) / (end - start))) * 306,
|
|
760
876
|
y: 104 - (point.spend / maxSpend) * 78,
|
|
761
877
|
point,
|
|
762
878
|
}));
|
|
763
879
|
const sum = visible.reduce((total, point) => total + point.spend, 0);
|
|
764
|
-
const
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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)}`),
|
|
770
897
|
React.createElement("text", { key: "y-zero", x: 36, y: 108, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, "$0"),
|
|
771
|
-
|
|
898
|
+
);
|
|
772
899
|
if (chartPoints.length > 0) {
|
|
773
|
-
const linePoints = chartPoints.length === 1
|
|
774
|
-
? `${Math.max(42, chartPoints[0].x - 16)},${chartPoints[0].y} ${chartPoints[0].x},${chartPoints[0].y}`
|
|
775
|
-
: chartPoints.map(({ x, y }) => `${x},${y}`).join(" ");
|
|
776
900
|
children.push(React.createElement("polyline", {
|
|
777
|
-
key: "line", points:
|
|
778
|
-
stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 2.5,
|
|
779
|
-
strokeLinecap: "round", strokeLinejoin: "round",
|
|
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",
|
|
780
903
|
}));
|
|
781
904
|
chartPoints.forEach(({ x, y, point }, index) => children.push(
|
|
782
905
|
React.createElement("g", { key: `point-${index}` },
|
|
783
|
-
React.createElement("circle", {
|
|
784
|
-
|
|
785
|
-
fill: "var(--dsw-alias-state-success-primary)",
|
|
786
|
-
}),
|
|
787
|
-
React.createElement("title", null, `${labelTime(point.at)} · $${point.spend.toFixed(2)}`),
|
|
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)}`),
|
|
788
908
|
),
|
|
789
909
|
));
|
|
790
|
-
const first = chartPoints[0].point.at;
|
|
791
|
-
const last = chartPoints[chartPoints.length - 1].point.at;
|
|
792
|
-
children.push(
|
|
793
|
-
React.createElement("text", { key: "x-first", x: 42, y: 126, fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, labelTime(first)),
|
|
794
|
-
React.createElement("text", { key: "x-last", x: 348, y: 126, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, labelTime(last)),
|
|
795
|
-
);
|
|
796
910
|
}
|
|
911
|
+
const months = storedMonths(points);
|
|
912
|
+
const activeMonth = months.includes(selectedMonth) ? selectedMonth : (months[0] ?? "");
|
|
797
913
|
return React.createElement("div", { className: "dsh-usage-balance-card dsh-usage-history" },
|
|
798
914
|
React.createElement("div", { className: "dsh-usage-history-head" },
|
|
799
915
|
React.createElement("span", { className: "dsh-usage-history-title" }, t("history.title")),
|
|
800
|
-
visible.length > 0
|
|
801
|
-
? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`)
|
|
802
|
-
: null,
|
|
916
|
+
visible.length > 0 ? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`) : null,
|
|
803
917
|
),
|
|
804
918
|
React.createElement("div", { className: "dsh-usage-history-ranges" }, HISTORY_RANGES.map((item) =>
|
|
805
|
-
React.createElement("button", {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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,
|
|
810
927
|
visible.length > 0
|
|
811
|
-
? React.createElement("svg", {
|
|
812
|
-
className: "dsh-usage-history-chart", viewBox: "0 0 360 132",
|
|
813
|
-
role: "img", "aria-label": t("history.title"),
|
|
814
|
-
}, children)
|
|
928
|
+
? React.createElement("svg", { className: "dsh-usage-history-chart", viewBox: "0 0 360 132", role: "img", "aria-label": t("history.title") }, children)
|
|
815
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
|
+
),
|
|
816
938
|
);
|
|
817
939
|
}
|
|
818
940
|
|
|
@@ -972,7 +1094,7 @@ window.__ModuleLoader__.load({
|
|
|
972
1094
|
);
|
|
973
1095
|
}
|
|
974
1096
|
|
|
975
|
-
function UsageBalance({ data, history, historyRange, onHistoryRange, onRefresh, t }) {
|
|
1097
|
+
function UsageBalance({ data, history, historyRange, customRange, appliedCustomRange, selectedMonth, onHistoryRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onResetHistory, onRefresh, t }) {
|
|
976
1098
|
let card;
|
|
977
1099
|
if (data.mode === "commandcode") card = React.createElement(CommandCodeCard, { data, t });
|
|
978
1100
|
else if (data.mode === "newapi") card = React.createElement(NewApiCard, { data, t });
|
|
@@ -986,6 +1108,8 @@ window.__ModuleLoader__.load({
|
|
|
986
1108
|
data.mode === "commandcode"
|
|
987
1109
|
? React.createElement(BalanceHistory, {
|
|
988
1110
|
points: history, range: historyRange, onRange: onHistoryRange, t,
|
|
1111
|
+
customRange, appliedCustomRange, selectedMonth, onCustomRange, onApplyCustomRange, onSelectMonth,
|
|
1112
|
+
onClearMonth, onReset: onResetHistory,
|
|
989
1113
|
})
|
|
990
1114
|
: null,
|
|
991
1115
|
React.createElement(UsageMeta, { data, t }),
|
|
@@ -1256,16 +1380,29 @@ window.__ModuleLoader__.load({
|
|
|
1256
1380
|
const [form, setForm] = React.useState({ auto: true, minutes: "5" });
|
|
1257
1381
|
const [view, setView] = React.useState({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1258
1382
|
const [history, setHistory] = React.useState([]);
|
|
1259
|
-
const [historyRange, setHistoryRange] = 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("");
|
|
1260
1393
|
const [, forceRender] = React.useState(0);
|
|
1261
1394
|
const viewRef = React.useRef(view);
|
|
1262
1395
|
const inFlight = React.useRef(false);
|
|
1396
|
+
const samplePending = React.useRef(false);
|
|
1263
1397
|
const generation = React.useRef(0);
|
|
1264
1398
|
viewRef.current = view;
|
|
1265
1399
|
|
|
1266
1400
|
/** One usage query; concurrent calls collapse into the first one. */
|
|
1267
|
-
const refresh = React.useCallback(() => {
|
|
1268
|
-
if (inFlight.current)
|
|
1401
|
+
const refresh = React.useCallback((sample = false) => {
|
|
1402
|
+
if (inFlight.current) {
|
|
1403
|
+
if (sample) samplePending.current = true;
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1269
1406
|
inFlight.current = true;
|
|
1270
1407
|
const gen = generation.current;
|
|
1271
1408
|
// A missing host route binding is reported in the panel, never thrown:
|
|
@@ -1282,9 +1419,17 @@ window.__ModuleLoader__.load({
|
|
|
1282
1419
|
// the panel no longer shows, so it must not write anything.
|
|
1283
1420
|
if (gen !== generation.current) return;
|
|
1284
1421
|
inFlight.current = false;
|
|
1422
|
+
const pendingSample = samplePending.current;
|
|
1423
|
+
samplePending.current = false;
|
|
1285
1424
|
if (result.ok) {
|
|
1286
1425
|
const sampledAt = Date.now();
|
|
1287
|
-
const
|
|
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
|
+
: [];
|
|
1288
1433
|
if (result.value.mode === "commandcode") setHistory(nextHistory);
|
|
1289
1434
|
setView((s) => ({ ...s, refreshing: false, data: result.value, error: "", updatedAt: sampledAt }));
|
|
1290
1435
|
} else {
|
|
@@ -1292,11 +1437,16 @@ window.__ModuleLoader__.load({
|
|
|
1292
1437
|
// blank out what the user was reading.
|
|
1293
1438
|
setView((s) => ({ ...s, refreshing: false, error: result.error.message }));
|
|
1294
1439
|
}
|
|
1440
|
+
if (pendingSample && !sample) setTimeout(() => { refresh(true); }, 0);
|
|
1295
1441
|
})
|
|
1296
1442
|
.catch((err) => {
|
|
1297
1443
|
if (gen !== generation.current) return;
|
|
1298
1444
|
inFlight.current = false;
|
|
1299
1445
|
setView((s) => ({ ...s, refreshing: false, error: String(err) }));
|
|
1446
|
+
if (samplePending.current) {
|
|
1447
|
+
samplePending.current = false;
|
|
1448
|
+
setTimeout(() => { refresh(true); }, 0);
|
|
1449
|
+
}
|
|
1300
1450
|
});
|
|
1301
1451
|
}, [fetchBalance, provider, model, effort, sessionId]);
|
|
1302
1452
|
|
|
@@ -1312,8 +1462,10 @@ window.__ModuleLoader__.load({
|
|
|
1312
1462
|
React.useEffect(() => {
|
|
1313
1463
|
generation.current += 1;
|
|
1314
1464
|
inFlight.current = false;
|
|
1465
|
+
samplePending.current = false;
|
|
1315
1466
|
setView({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1316
1467
|
setHistory([]);
|
|
1468
|
+
setSelectedMonth("");
|
|
1317
1469
|
refresh();
|
|
1318
1470
|
}, [refresh]);
|
|
1319
1471
|
|
|
@@ -1325,26 +1477,23 @@ window.__ModuleLoader__.load({
|
|
|
1325
1477
|
return () => { clearInterval(id); };
|
|
1326
1478
|
}, [config.auto, config.intervalMs, refresh]);
|
|
1327
1479
|
|
|
1328
|
-
// Sample
|
|
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.
|
|
1329
1482
|
React.useEffect(() => {
|
|
1330
1483
|
if (view.data?.mode !== "commandcode") return undefined;
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1333
|
-
const wait =
|
|
1334
|
-
|
|
1335
|
-
: Math.max(0, HISTORY_INTERVAL_MS - (Date.now() - last.at));
|
|
1336
|
-
let retryId;
|
|
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;
|
|
1337
1488
|
const sampleId = setTimeout(() => {
|
|
1338
|
-
refresh();
|
|
1339
|
-
|
|
1340
|
-
if (!inFlight.current) refresh();
|
|
1341
|
-
}, 5 * 60 * 1000);
|
|
1489
|
+
refresh(true);
|
|
1490
|
+
intervalId = setInterval(() => { if (!inFlight.current) refresh(true); }, HISTORY_INTERVAL_MS);
|
|
1342
1491
|
}, wait);
|
|
1343
1492
|
return () => {
|
|
1344
1493
|
clearTimeout(sampleId);
|
|
1345
|
-
if (
|
|
1494
|
+
if (intervalId !== undefined) clearInterval(intervalId);
|
|
1346
1495
|
};
|
|
1347
|
-
}, [view.data,
|
|
1496
|
+
}, [view.data, refresh]);
|
|
1348
1497
|
|
|
1349
1498
|
// Opening the panel: show what is known, and only pay for a query when the
|
|
1350
1499
|
// numbers are stale (which also renders as 刷新中 over those numbers).
|
|
@@ -1433,6 +1582,28 @@ window.__ModuleLoader__.load({
|
|
|
1433
1582
|
setOpen(true);
|
|
1434
1583
|
};
|
|
1435
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
|
+
|
|
1436
1607
|
let body;
|
|
1437
1608
|
if (view.data !== null) {
|
|
1438
1609
|
body = React.createElement("div", { className: "dsh-usage-stack" },
|
|
@@ -1440,7 +1611,15 @@ window.__ModuleLoader__.load({
|
|
|
1440
1611
|
data: view.data,
|
|
1441
1612
|
history,
|
|
1442
1613
|
historyRange,
|
|
1614
|
+
customRange,
|
|
1615
|
+
appliedCustomRange,
|
|
1616
|
+
selectedMonth,
|
|
1443
1617
|
onHistoryRange: setHistoryRange,
|
|
1618
|
+
onCustomRange: setCustomRange,
|
|
1619
|
+
onApplyCustomRange: applyCustomRange,
|
|
1620
|
+
onSelectMonth: setSelectedMonth,
|
|
1621
|
+
onClearMonth: clearMonth,
|
|
1622
|
+
onResetHistory: resetHistory,
|
|
1444
1623
|
onRefresh: refresh,
|
|
1445
1624
|
t,
|
|
1446
1625
|
}),
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Model usage",
|
|
4
|
-
"description": "Adds a Usage entry to the session header
|
|
4
|
+
"description": "Adds a Usage entry to the session header with provider usage, ten-minute Command Code sampling, calendar-month trends, custom ranges and history cleanup."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "用量",
|
|
4
|
-
"description": "在会话头部加一个「用量」入口:查看当前对话所用模型对应供应商账户的用量(余额 / 套餐窗口 /
|
|
4
|
+
"description": "在会话头部加一个「用量」入口:查看当前对话所用模型对应供应商账户的用量(余额 / 套餐窗口 / 请求数),支持 Command Code 十分钟采样、日历月历史趋势、自定义时间段和记录清理。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-usage",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Provider-account usage for DeepSeek Harness: quota windows, credit balance and request counts for the model a conversation runs on, with per-provider adapters and manual target selection.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "liuchaoxu <ischaoxu@163.com>",
|
|
Binary file
|