@linxin666/dsh-usage 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,755 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@linxin666/dsh-usage",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region \0dsh-store-engine
10
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
11
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
12
+ let engine;
13
+ try {
14
+ engine = require(platform);
15
+ } catch {
16
+ engine = require(legacy);
17
+ }
18
+ engine.createSnapshotStore;
19
+ const defineStore = engine.defineStore;
20
+ engine.shallowEqual;
21
+ //#endregion
22
+ //#region src/client/usage-store.ts
23
+ /**
24
+ * Browser-side usage store: the overview snapshot polled from the host plus
25
+ * the fetch lifecycle. Section-local: the store lives while the settings
26
+ * section is mounted, so polling only runs while the page is open.
27
+ * @module @linxin666/dsh-usage/client/usage-store
28
+ */
29
+ /** Create the usage store handle (apply world only; never module-level). */
30
+ function createUsageStore() {
31
+ return defineStore({
32
+ init: () => ({
33
+ snapshot: null,
34
+ status: "loading",
35
+ error: null
36
+ }),
37
+ actions: {
38
+ setSnapshot: (draft, snapshot) => {
39
+ draft.snapshot = snapshot;
40
+ draft.status = "ready";
41
+ draft.error = null;
42
+ },
43
+ setState: (draft, status, error) => {
44
+ draft.status = status;
45
+ draft.error = error;
46
+ }
47
+ }
48
+ });
49
+ }
50
+ //#endregion
51
+ //#region src/client/locales.ts
52
+ /**
53
+ * dsh-usage locale dictionaries (zh/en). The zh dictionary is the key source;
54
+ * `en` mirrors its full key set (packages/AGENTS.md bilingual discipline).
55
+ * @module @linxin666/dsh-usage/client/locales
56
+ */
57
+ /** Dictionary namespace this package registers. */
58
+ const NS = "dsh-web-ui-usage";
59
+ /** Chinese copy. */
60
+ const zh = {
61
+ "usage.title": "使用统计",
62
+ "usage.tab.usage": "用量",
63
+ "usage.tab.plans": "个人套餐",
64
+ "usage.refresh": "刷新",
65
+ "usage.refreshing": "刷新中…",
66
+ "usage.updated": "更新于 {time}",
67
+ "usage.loading": "正在加载用量数据…",
68
+ "usage.error": "加载失败:{error}",
69
+ "usage.current": "当前",
70
+ "usage.today": "今日用量",
71
+ "usage.calls": "{n} 次调用",
72
+ "usage.tokens.total": "总 tokens",
73
+ "usage.tokens.input": "输入",
74
+ "usage.tokens.output": "输出",
75
+ "usage.tokens.cacheRead": "缓存读",
76
+ "usage.tokens.cacheWrite": "缓存写",
77
+ "usage.trend": "近 30 天",
78
+ "usage.noData": "暂无用量数据(统计自插件启用起)",
79
+ "usage.balance": "余额",
80
+ "usage.balance.unsupported": "暂不支持余额查询",
81
+ "usage.balance.noCredential": "未配置凭据",
82
+ "usage.oauth": "OAuth 凭据,不做余额查询",
83
+ "usage.plan.reset": "{date} 重置",
84
+ "usage.plan.noPlan": "未检测到套餐数据",
85
+ "usage.plan.windows.5h": "5 小时",
86
+ "usage.plan.windows.week": "每周",
87
+ "usage.plan.windows.month": "每月",
88
+ "usage.provider.error": "查询失败:{error}",
89
+ "usage.config.title": "设置",
90
+ "usage.config.enabled": "启用插件",
91
+ "usage.config.pollIntervalSec": "轮询间隔(秒)",
92
+ "usage.config.bubbleMode": "宠物气泡",
93
+ "usage.config.bubbleMode.always": "常驻显示",
94
+ "usage.config.bubbleMode.change": "仅变化时",
95
+ "usage.config.bubbleMode.off": "关闭"
96
+ };
97
+ /** English mirror; every zh key present. */
98
+ const en = {
99
+ "usage.title": "Usage Statistics",
100
+ "usage.tab.usage": "Usage",
101
+ "usage.tab.plans": "Plans",
102
+ "usage.refresh": "Refresh",
103
+ "usage.refreshing": "Refreshing…",
104
+ "usage.updated": "Updated {time}",
105
+ "usage.loading": "Loading usage data…",
106
+ "usage.error": "Failed to load: {error}",
107
+ "usage.current": "Current",
108
+ "usage.today": "Today",
109
+ "usage.calls": "{n} calls",
110
+ "usage.tokens.total": "Total tokens",
111
+ "usage.tokens.input": "Input",
112
+ "usage.tokens.output": "Output",
113
+ "usage.tokens.cacheRead": "Cache read",
114
+ "usage.tokens.cacheWrite": "Cache write",
115
+ "usage.trend": "Last 30 days",
116
+ "usage.noData": "No usage data yet (counting starts when the plugin is enabled)",
117
+ "usage.balance": "Balance",
118
+ "usage.balance.unsupported": "Balance query not supported",
119
+ "usage.balance.noCredential": "No credential configured",
120
+ "usage.oauth": "OAuth credential, no balance query",
121
+ "usage.plan.reset": "resets {date}",
122
+ "usage.plan.noPlan": "No plan data detected",
123
+ "usage.plan.windows.5h": "5 hours",
124
+ "usage.plan.windows.week": "Weekly",
125
+ "usage.plan.windows.month": "Monthly",
126
+ "usage.provider.error": "Query failed: {error}",
127
+ "usage.config.title": "Settings",
128
+ "usage.config.enabled": "Enable plugin",
129
+ "usage.config.pollIntervalSec": "Poll interval (seconds)",
130
+ "usage.config.bubbleMode": "Pet bubble",
131
+ "usage.config.bubbleMode.always": "Always visible",
132
+ "usage.config.bubbleMode.change": "On change",
133
+ "usage.config.bubbleMode.off": "Off"
134
+ };
135
+ /**
136
+ * Active dictionary, picked by the document language at call time. The
137
+ * section resolves its copy the same tiny way the pet's DOM-injected surface
138
+ * does (the settings section has no framework locale seat of its own).
139
+ */
140
+ function dictionary() {
141
+ return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
142
+ }
143
+ /** Translate a key with optional `{name}` template params; missing keys degrade to the key. */
144
+ function t(key, params) {
145
+ let text = dictionary()[key] ?? key;
146
+ if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
147
+ return text;
148
+ }
149
+ //#endregion
150
+ //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
151
+ const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_trend{align-items:flex-end;gap:3px;height:56px;display:flex}.cvtkAW_trendBar{background:color-mix(in srgb, currentColor 30%, transparent);border-radius:3px 3px 0 0;flex:1 1 0;min-width:4px;min-height:2px}.cvtkAW_trendBarToday{background:color-mix(in srgb, currentColor 65%, transparent)}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}";
152
+ const tagId = "@linxin666/dsh-usage/usage.module.css";
153
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
154
+ const tag = document.createElement("style");
155
+ tag.dataset.plugin = "@linxin666/dsh-usage";
156
+ tag.dataset.pluginCss = tagId;
157
+ tag.textContent = css;
158
+ document.head.appendChild(tag);
159
+ }
160
+ var usage_module_css_default = {
161
+ "bar": "cvtkAW_bar",
162
+ "barFill": "cvtkAW_barFill",
163
+ "barLow": "cvtkAW_barLow",
164
+ "barWarn": "cvtkAW_barWarn",
165
+ "card": "cvtkAW_card",
166
+ "cardTitle": "cvtkAW_cardTitle",
167
+ "currentBadge": "cvtkAW_currentBadge",
168
+ "currentProvider": "cvtkAW_currentProvider",
169
+ "errorLine": "cvtkAW_errorLine",
170
+ "header": "cvtkAW_header",
171
+ "muted": "cvtkAW_muted",
172
+ "planCard": "cvtkAW_planCard",
173
+ "planHead": "cvtkAW_planHead",
174
+ "planName": "cvtkAW_planName",
175
+ "providerBalance": "cvtkAW_providerBalance",
176
+ "providerName": "cvtkAW_providerName",
177
+ "providerRow": "cvtkAW_providerRow",
178
+ "providerTokens": "cvtkAW_providerTokens",
179
+ "refreshBtn": "cvtkAW_refreshBtn",
180
+ "resetLine": "cvtkAW_resetLine",
181
+ "section": "cvtkAW_section",
182
+ "settingItem": "cvtkAW_settingItem",
183
+ "settingsGrid": "cvtkAW_settingsGrid",
184
+ "stat": "cvtkAW_stat",
185
+ "statLabel": "cvtkAW_statLabel",
186
+ "statRow": "cvtkAW_statRow",
187
+ "statValue": "cvtkAW_statValue",
188
+ "tab": "cvtkAW_tab",
189
+ "tabActive": "cvtkAW_tabActive",
190
+ "tabs": "cvtkAW_tabs",
191
+ "trend": "cvtkAW_trend",
192
+ "trendAxis": "cvtkAW_trendAxis",
193
+ "trendBar": "cvtkAW_trendBar",
194
+ "trendBarToday": "cvtkAW_trendBarToday",
195
+ "windowLabel": "cvtkAW_windowLabel",
196
+ "windowRow": "cvtkAW_windowRow"
197
+ };
198
+ //#endregion
199
+ //#region src/client/UsageSectionCard.tsx
200
+ /**
201
+ * The usage statistics settings section: two tabs (用量: today's usage,
202
+ * balances, trend; 个人套餐: per-provider plan quota windows) plus a compact
203
+ * settings row. Data comes from the host's loopback-fenced
204
+ * /api/dsh-usage/overview document; polling runs only while the section is
205
+ * mounted and the tab is visible.
206
+ * @module @linxin666/dsh-usage/client/UsageSectionCard
207
+ */
208
+ /** Poll cadence while the section is open. */
209
+ const SECTION_POLL_MS = 1e4;
210
+ /** Compact token count: 12345 -> 12.3k, 1234567 -> 1.23M. */
211
+ function formatTokens(value) {
212
+ if (!Number.isFinite(value) || value <= 0) return "0";
213
+ if (value < 1e3) return String(value);
214
+ if (value < 1e6) return trim(value / 1e3) + "k";
215
+ if (value < 1e9) return trim(value / 1e6) + "M";
216
+ return trim(value / 1e9) + "B";
217
+ }
218
+ function trim(value) {
219
+ return value >= 100 ? String(Math.round(value)) : value.toFixed(value >= 10 ? 1 : 2).replace(/\.?0+$/, "");
220
+ }
221
+ function formatTime(ms) {
222
+ try {
223
+ return new Date(ms).toLocaleTimeString();
224
+ } catch {
225
+ return "";
226
+ }
227
+ }
228
+ function toneClass(percent) {
229
+ if (percent >= 90) return usage_module_css_default.barLow;
230
+ if (percent >= 70) return usage_module_css_default.barWarn;
231
+ return usage_module_css_default.barFill;
232
+ }
233
+ function TotalsRow(props) {
234
+ const { totals } = props;
235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
236
+ className: usage_module_css_default.statRow,
237
+ children: [
238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
239
+ className: usage_module_css_default.stat,
240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
241
+ className: usage_module_css_default.statValue,
242
+ children: formatTokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens)
243
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
244
+ className: usage_module_css_default.statLabel,
245
+ children: t("usage.tokens.total")
246
+ })]
247
+ }),
248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
249
+ className: usage_module_css_default.stat,
250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
251
+ className: usage_module_css_default.statValue,
252
+ children: formatTokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens)
253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
254
+ className: usage_module_css_default.statLabel,
255
+ children: t("usage.tokens.input")
256
+ })]
257
+ }),
258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
+ className: usage_module_css_default.stat,
260
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
261
+ className: usage_module_css_default.statValue,
262
+ children: formatTokens(totals.outputTokens)
263
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
264
+ className: usage_module_css_default.statLabel,
265
+ children: t("usage.tokens.output")
266
+ })]
267
+ }),
268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
269
+ className: usage_module_css_default.stat,
270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
271
+ className: usage_module_css_default.statValue,
272
+ children: formatTokens(totals.cacheReadTokens)
273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
274
+ className: usage_module_css_default.statLabel,
275
+ children: t("usage.tokens.cacheRead")
276
+ })]
277
+ }),
278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
279
+ className: usage_module_css_default.stat,
280
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
281
+ className: usage_module_css_default.statValue,
282
+ children: formatTokens(totals.cacheWriteTokens)
283
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
+ className: usage_module_css_default.statLabel,
285
+ children: t("usage.tokens.cacheWrite")
286
+ })]
287
+ }),
288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
289
+ className: usage_module_css_default.stat,
290
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
291
+ className: usage_module_css_default.statValue,
292
+ children: formatTokens(totals.calls)
293
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
294
+ className: usage_module_css_default.statLabel,
295
+ children: t("usage.calls", { n: totals.calls })
296
+ })]
297
+ })
298
+ ]
299
+ });
300
+ }
301
+ function balanceLine(provider) {
302
+ if (provider.balance !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
303
+ className: usage_module_css_default.providerBalance,
304
+ children: [
305
+ provider.balance.currency.toUpperCase() === "CNY" ? "¥" : provider.balance.currency.toUpperCase() === "USD" ? "$" : "",
306
+ provider.balance.totalBalance,
307
+ provider.balance.currency.toUpperCase() !== "CNY" && provider.balance.currency.toUpperCase() !== "USD" ? " " + provider.balance.currency.toUpperCase() : ""
308
+ ]
309
+ });
310
+ if (provider.credential === "oauth") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
311
+ className: usage_module_css_default.muted,
312
+ children: t("usage.oauth")
313
+ });
314
+ if (provider.credential === "none") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
315
+ className: usage_module_css_default.muted,
316
+ children: t("usage.balance.noCredential")
317
+ });
318
+ if (!provider.supported) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
319
+ className: usage_module_css_default.muted,
320
+ children: t("usage.balance.unsupported")
321
+ });
322
+ return null;
323
+ }
324
+ function ProviderRow(props) {
325
+ const { provider, current } = props;
326
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
327
+ className: usage_module_css_default.providerRow,
328
+ "data-dsh-part": "provider-row",
329
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
330
+ className: usage_module_css_default.providerName,
331
+ children: [provider.displayName, current === provider.provider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
332
+ className: usage_module_css_default.currentBadge,
333
+ children: t("usage.current")
334
+ })]
335
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
336
+ className: usage_module_css_default.providerTokens,
337
+ children: balanceLine(provider)
338
+ })]
339
+ });
340
+ }
341
+ /** The section component; the slot merges the face into these props. */
342
+ function UsageSectionCard(props) {
343
+ const { store, poll, refresh, settings } = props;
344
+ const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
345
+ const settingsSnapshot = settings.getSnapshot();
346
+ const settingsValue = settingsSnapshot.value ?? {};
347
+ const [tab, setTab] = (0, react.useState)("usage");
348
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
349
+ (0, react.useEffect)(() => {
350
+ poll();
351
+ let timer;
352
+ const start = () => {
353
+ if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, SECTION_POLL_MS);
354
+ };
355
+ const onVisibility = () => {
356
+ if (document.visibilityState === "visible") {
357
+ poll();
358
+ start();
359
+ } else if (timer !== void 0) {
360
+ window.clearInterval(timer);
361
+ timer = void 0;
362
+ }
363
+ };
364
+ start();
365
+ document.addEventListener("visibilitychange", onVisibility);
366
+ return () => {
367
+ if (timer !== void 0) window.clearInterval(timer);
368
+ document.removeEventListener("visibilitychange", onVisibility);
369
+ };
370
+ }, [poll]);
371
+ const snapshot = ui.snapshot;
372
+ const trendMax = (0, react.useMemo)(() => {
373
+ if (snapshot === null) return 0;
374
+ return Math.max(1, ...snapshot.usage.days.map((day) => day.totals.inputTokens + day.totals.outputTokens + day.totals.cacheReadTokens + day.totals.cacheWriteTokens));
375
+ }, [snapshot]);
376
+ const onRefresh = () => {
377
+ setRefreshing(true);
378
+ try {
379
+ refresh();
380
+ } finally {
381
+ window.setTimeout(() => setRefreshing(false), 3e3);
382
+ }
383
+ };
384
+ if (ui.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
385
+ className: usage_module_css_default.section,
386
+ "data-dsh-plugin": "usage",
387
+ children: t("usage.error", { error: ui.error ?? "" })
388
+ });
389
+ if (snapshot === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
390
+ className: usage_module_css_default.section,
391
+ "data-dsh-plugin": "usage",
392
+ children: t("usage.loading")
393
+ });
394
+ const current = snapshot.current;
395
+ const currentProvider = snapshot.providers.find((provider) => provider.provider === current.provider);
396
+ const planProviders = snapshot.providers.filter((provider) => provider.plan !== void 0 || provider.supported && provider.credential !== "none" && provider.credential !== "oauth");
397
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
+ className: usage_module_css_default.section,
399
+ "data-dsh-plugin": "usage",
400
+ children: [
401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
402
+ className: usage_module_css_default.header,
403
+ "data-dsh-part": "header",
404
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
405
+ className: usage_module_css_default.currentProvider,
406
+ children: currentProvider !== void 0 ? `${currentProvider.displayName}${current.model !== void 0 && current.model !== "" ? " · " + current.model : ""}` : t("usage.noData")
407
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
408
+ style: {
409
+ display: "flex",
410
+ gap: 8,
411
+ alignItems: "center"
412
+ },
413
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
414
+ className: usage_module_css_default.muted,
415
+ children: t("usage.updated", { time: formatTime(snapshot.updatedAt) })
416
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
417
+ type: "button",
418
+ className: usage_module_css_default.refreshBtn,
419
+ onClick: onRefresh,
420
+ disabled: refreshing,
421
+ children: refreshing ? t("usage.refreshing") : t("usage.refresh")
422
+ })]
423
+ })]
424
+ }),
425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
426
+ className: usage_module_css_default.tabs,
427
+ role: "tablist",
428
+ "data-dsh-part": "tabs",
429
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
430
+ type: "button",
431
+ role: "tab",
432
+ "aria-selected": tab === "usage",
433
+ className: tab === "usage" ? `${usage_module_css_default.tab} ${usage_module_css_default.tabActive}` : usage_module_css_default.tab,
434
+ onClick: () => setTab("usage"),
435
+ children: t("usage.tab.usage")
436
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
437
+ type: "button",
438
+ role: "tab",
439
+ "aria-selected": tab === "plans",
440
+ className: tab === "plans" ? `${usage_module_css_default.tab} ${usage_module_css_default.tabActive}` : usage_module_css_default.tab,
441
+ onClick: () => setTab("plans"),
442
+ children: t("usage.tab.plans")
443
+ })]
444
+ }),
445
+ tab === "usage" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
446
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
447
+ className: usage_module_css_default.card,
448
+ "data-dsh-part": "today-card",
449
+ children: [
450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
451
+ className: usage_module_css_default.cardTitle,
452
+ children: t("usage.today")
453
+ }),
454
+ snapshot.usage.today.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
455
+ className: usage_module_css_default.muted,
456
+ children: t("usage.noData")
457
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TotalsRow, { totals: snapshot.usage.today.totals }),
458
+ snapshot.usage.today.providers.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
459
+ "data-dsh-part": "provider-list",
460
+ children: snapshot.usage.today.providers.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
461
+ className: usage_module_css_default.providerRow,
462
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
463
+ className: usage_module_css_default.providerName,
464
+ children: [snapshot.providers.find((provider) => provider.provider === row.provider)?.displayName ?? row.provider, current.provider === row.provider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
465
+ className: usage_module_css_default.currentBadge,
466
+ children: t("usage.current")
467
+ })]
468
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
469
+ className: usage_module_css_default.providerTokens,
470
+ children: [
471
+ formatTokens(row.totals.inputTokens + row.totals.cacheReadTokens + row.totals.cacheWriteTokens + row.totals.outputTokens),
472
+ " · ",
473
+ t("usage.calls", { n: row.totals.calls })
474
+ ]
475
+ })]
476
+ }, row.provider))
477
+ })
478
+ ]
479
+ }),
480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
481
+ className: usage_module_css_default.card,
482
+ "data-dsh-part": "balance-card",
483
+ children: [
484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
485
+ className: usage_module_css_default.cardTitle,
486
+ children: t("usage.balance")
487
+ }),
488
+ snapshot.providers.filter((provider) => provider.supported).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
489
+ className: usage_module_css_default.muted,
490
+ children: t("usage.balance.unsupported")
491
+ }) : snapshot.providers.filter((provider) => provider.supported).map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderRow, {
492
+ provider,
493
+ current: current.provider
494
+ }, provider.provider)),
495
+ snapshot.providers.some((provider) => provider.error !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
496
+ className: usage_module_css_default.errorLine,
497
+ children: snapshot.providers.filter((provider) => provider.error !== void 0).map((provider) => `${provider.displayName}: ${t("usage.provider.error", { error: provider.error ?? "" })}`).join(";")
498
+ })
499
+ ]
500
+ }),
501
+ snapshot.usage.days.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
502
+ className: usage_module_css_default.card,
503
+ "data-dsh-part": "trend-card",
504
+ children: [
505
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
506
+ className: usage_module_css_default.cardTitle,
507
+ children: t("usage.trend")
508
+ }),
509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
510
+ className: usage_module_css_default.trend,
511
+ children: snapshot.usage.days.map((day, index) => {
512
+ const total = day.totals.inputTokens + day.totals.outputTokens + day.totals.cacheReadTokens + day.totals.cacheWriteTokens;
513
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
514
+ className: index === snapshot.usage.days.length - 1 ? `${usage_module_css_default.trendBar} ${usage_module_css_default.trendBarToday}` : usage_module_css_default.trendBar,
515
+ style: { height: `${Math.max(3, Math.round(total / trendMax * 100))}%` },
516
+ title: `${day.date}: ${formatTokens(total)}`
517
+ }, day.date);
518
+ })
519
+ }),
520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
521
+ className: usage_module_css_default.trendAxis,
522
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: snapshot.usage.days[0]?.date.slice(5) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: snapshot.usage.days[snapshot.usage.days.length - 1]?.date.slice(5) })]
523
+ })
524
+ ]
525
+ }),
526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
527
+ settings,
528
+ snapshot: settingsSnapshot.status === "ready" ? settingsSnapshot : void 0,
529
+ value: settingsValue
530
+ })
531
+ ] }),
532
+ tab === "plans" && (planProviders.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
533
+ className: usage_module_css_default.card,
534
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
535
+ className: usage_module_css_default.muted,
536
+ children: t("usage.plan.noPlan")
537
+ })
538
+ }) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, { provider }, provider.provider)))
539
+ ]
540
+ });
541
+ }
542
+ function PlanCard(props) {
543
+ const { provider } = props;
544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
545
+ className: `${usage_module_css_default.card} ${usage_module_css_default.planCard}`,
546
+ "data-dsh-part": "plan-card",
547
+ children: [
548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
549
+ className: usage_module_css_default.planHead,
550
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
551
+ className: usage_module_css_default.planName,
552
+ children: [provider.displayName, provider.plan?.planName !== void 0 ? ` · ${provider.plan.planName}` : ""]
553
+ })
554
+ }),
555
+ provider.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
556
+ className: usage_module_css_default.errorLine,
557
+ children: t("usage.provider.error", { error: provider.error })
558
+ }),
559
+ provider.plan === void 0 || provider.plan.windows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
560
+ className: usage_module_css_default.muted,
561
+ children: t("usage.plan.noPlan")
562
+ }) : provider.plan.windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
563
+ className: usage_module_css_default.windowRow,
564
+ "data-dsh-part": "plan-window",
565
+ children: [
566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
567
+ className: usage_module_css_default.windowLabel,
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.name ?? t(`usage.plan.windows.${window.key}`) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.percent !== void 0 ? `${window.percent >= 10 ? Math.round(window.percent) : window.percent.toFixed(1)}%` : "" })]
569
+ }),
570
+ window.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
571
+ className: usage_module_css_default.bar,
572
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
573
+ className: toneClass(window.percent),
574
+ style: {
575
+ width: `${Math.min(100, Math.max(0, window.percent))}%`,
576
+ display: "block"
577
+ }
578
+ })
579
+ }),
580
+ window.resetsAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
581
+ className: usage_module_css_default.resetLine,
582
+ children: t("usage.plan.reset", { date: new Date(window.resetsAt).toLocaleString() })
583
+ })
584
+ ]
585
+ }, window.key))
586
+ ]
587
+ });
588
+ }
589
+ function SettingsRow(props) {
590
+ const { settings, snapshot, value } = props;
591
+ const disabled = snapshot === void 0 || !snapshot.writable;
592
+ const bubbleMode = typeof value.bubbleMode === "string" && [
593
+ "always",
594
+ "change",
595
+ "off"
596
+ ].includes(value.bubbleMode) ? value.bubbleMode : "always";
597
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
598
+ className: usage_module_css_default.card,
599
+ "data-dsh-part": "settings-row",
600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
601
+ className: usage_module_css_default.cardTitle,
602
+ children: t("usage.config.title")
603
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
604
+ className: usage_module_css_default.settingsGrid,
605
+ children: [
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
607
+ className: usage_module_css_default.settingItem,
608
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
609
+ type: "checkbox",
610
+ checked: value.enabled ?? true,
611
+ disabled,
612
+ onChange: (event) => {
613
+ settings.set("enabled", event.target.checked);
614
+ }
615
+ }), t("usage.config.enabled")]
616
+ }),
617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
618
+ className: usage_module_css_default.settingItem,
619
+ children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
620
+ type: "number",
621
+ min: 30,
622
+ max: 3600,
623
+ value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
624
+ disabled,
625
+ onChange: (event) => {
626
+ const parsed = Number(event.target.value);
627
+ if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) settings.set("pollIntervalSec", Math.round(parsed));
628
+ }
629
+ })]
630
+ }),
631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
632
+ className: usage_module_css_default.settingItem,
633
+ children: [t("usage.config.bubbleMode"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
634
+ value: bubbleMode,
635
+ disabled,
636
+ onChange: (event) => {
637
+ settings.set("bubbleMode", event.target.value);
638
+ },
639
+ children: [
640
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
641
+ value: "always",
642
+ children: t("usage.config.bubbleMode.always")
643
+ }),
644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
645
+ value: "change",
646
+ children: t("usage.config.bubbleMode.change")
647
+ }),
648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
649
+ value: "off",
650
+ children: t("usage.config.bubbleMode.off")
651
+ })
652
+ ]
653
+ })]
654
+ })
655
+ ]
656
+ })]
657
+ });
658
+ }
659
+ //#endregion
660
+ //#region src/client/index.ts
661
+ async function usageFetch(path, method) {
662
+ const response = await fetch(path, method === "POST" ? { method: "POST" } : {});
663
+ if (!response.ok) throw new Error("usage " + path + " failed: " + response.status);
664
+ return await response.json();
665
+ }
666
+ const usageApi = {
667
+ overview: () => usageFetch("/api/dsh-usage/overview", "GET"),
668
+ refresh: () => usageFetch("/api/dsh-usage/refresh", "POST")
669
+ };
670
+ /** Settings namespace the section edits (the host plugin registers it). */
671
+ const USAGE_SETTINGS_NS = "dsh-usage";
672
+ /** First-level nav position: directly below the Workshop section (order 150). */
673
+ const SECTION_ORDER = 151;
674
+ /** Required services. */
675
+ const inject = [
676
+ "slots",
677
+ "locale",
678
+ "connection",
679
+ "settingsScope",
680
+ "remote"
681
+ ];
682
+ /**
683
+ * Client plugin body: register dictionaries and seat the settings section.
684
+ * The overview poll loop and the store live with the section component's
685
+ * mount cycle, so no background traffic exists while the page is closed.
686
+ * @param ctx - client root context.
687
+ */
688
+ function apply(ctx) {
689
+ ctx.effect(() => {
690
+ try {
691
+ return ctx.locale.register(NS, {
692
+ zh,
693
+ en
694
+ });
695
+ } catch {
696
+ return () => {};
697
+ }
698
+ }, "dsh-usage: dictionaries");
699
+ const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: USAGE_SETTINGS_NS });
700
+ const store = createUsageStore().create();
701
+ let pollSeq = 0;
702
+ const poll = () => {
703
+ const seq = pollSeq + 1;
704
+ pollSeq = seq;
705
+ usageApi.overview().then((snapshot) => {
706
+ if (seq !== pollSeq) return;
707
+ store.actions.setSnapshot(snapshot);
708
+ }, () => {
709
+ if (seq !== pollSeq) return;
710
+ store.actions.setState("error", "usage.overview transport error");
711
+ });
712
+ };
713
+ const refresh = () => {
714
+ const seq = pollSeq + 1;
715
+ pollSeq = seq;
716
+ usageApi.refresh().then((snapshot) => {
717
+ if (seq !== pollSeq) return;
718
+ store.actions.setSnapshot(snapshot);
719
+ }, () => {
720
+ if (seq !== pollSeq) return;
721
+ store.actions.setState("error", "usage.refresh transport error");
722
+ });
723
+ };
724
+ const face = () => ({
725
+ store,
726
+ poll,
727
+ refresh,
728
+ settings: settingsScope
729
+ });
730
+ ctx.slots.inject("settings.section", () => {
731
+ try {
732
+ const unregister = ctx.slots.register({
733
+ name: "settings.section",
734
+ id: "dsh-usage",
735
+ order: SECTION_ORDER,
736
+ label: () => ctx.locale.bind(NS)("usage.title"),
737
+ locale: NS,
738
+ inject: face
739
+ }, UsageSectionCard);
740
+ return () => {
741
+ unregister();
742
+ };
743
+ } catch {
744
+ return () => {};
745
+ }
746
+ });
747
+ }
748
+ //#endregion
749
+ exports.apply = apply;
750
+ exports.inject = inject;
751
+ return module.exports;
752
+ }
753
+ });
754
+
755
+ //# sourceMappingURL=client.js.map