@dungsil/dsh-plugin-ui-usage 0.0.0-stage → 0.1.0
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/cordis.patch.yml +3 -0
- package/dist/client.js +387 -0
- package/dist/index.js +158 -0
- package/locale/en.json +6 -0
- package/locale/ko.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +92 -3
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-usage", factory(require) { const module = { exports: {} }; const exports = module.exports;
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
// ../../tools/ui/src/client.ts
|
|
7
|
+
function useLocaleSnapshot(locale) {
|
|
8
|
+
const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
|
|
9
|
+
const getSnapshot = (0, react.useCallback)(() => locale.getSnapshot(), [locale]);
|
|
10
|
+
return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
11
|
+
}
|
|
12
|
+
/** Each effect owns its sheet, even when another plugin installs the same defaults. */
|
|
13
|
+
function installStyles(css) {
|
|
14
|
+
const style = document.createElement("style");
|
|
15
|
+
style.textContent = css;
|
|
16
|
+
document.head.append(style);
|
|
17
|
+
return () => style.remove();
|
|
18
|
+
}
|
|
19
|
+
// src/copy.ts
|
|
20
|
+
const copy = {
|
|
21
|
+
en: {
|
|
22
|
+
nav: "Usage",
|
|
23
|
+
period: "Period",
|
|
24
|
+
week: "Last 7 days",
|
|
25
|
+
month: "Last 30 days",
|
|
26
|
+
all: "All time",
|
|
27
|
+
refresh: "Refresh",
|
|
28
|
+
loading: "Loading usage…",
|
|
29
|
+
error: "Could not load usage. Try refreshing.",
|
|
30
|
+
empty: "No recorded model calls in this period.",
|
|
31
|
+
total: "Total tokens",
|
|
32
|
+
input: "Uncached input",
|
|
33
|
+
output: "Output",
|
|
34
|
+
cacheRead: "Cache read",
|
|
35
|
+
cacheWrite: "Cache write",
|
|
36
|
+
calls: "Model calls",
|
|
37
|
+
sessions: "Sessions",
|
|
38
|
+
daily: "Daily tokens",
|
|
39
|
+
recent: "Daily tokens · last 30 days",
|
|
40
|
+
models: "By provider / model",
|
|
41
|
+
unknown: "Unknown",
|
|
42
|
+
updated: "Updated",
|
|
43
|
+
unreported: "Calls without token counts",
|
|
44
|
+
unreadable: "Sessions that could not be read",
|
|
45
|
+
scope: "Recorded session calls only, including subagents and retries. Forked history is counted once. Deleted sessions and calls outside session logs (such as titles) are excluded. This is not a billing statement.",
|
|
46
|
+
accounting: "Input excludes cache reads and writes. Reasoning is included in output. Totals use the provider total when available."
|
|
47
|
+
},
|
|
48
|
+
ko: {
|
|
49
|
+
nav: "사용량",
|
|
50
|
+
period: "기간",
|
|
51
|
+
week: "최근 7일",
|
|
52
|
+
month: "최근 30일",
|
|
53
|
+
all: "전체 기간",
|
|
54
|
+
refresh: "새로고침",
|
|
55
|
+
loading: "사용량을 불러오는 중입니다…",
|
|
56
|
+
error: "사용량을 불러오지 못했습니다. 새로고침해 주세요.",
|
|
57
|
+
empty: "이 기간에 기록된 모델 호출이 없습니다.",
|
|
58
|
+
total: "전체 토큰",
|
|
59
|
+
input: "캐시 제외 입력",
|
|
60
|
+
output: "출력",
|
|
61
|
+
cacheRead: "캐시 읽기",
|
|
62
|
+
cacheWrite: "캐시 쓰기",
|
|
63
|
+
calls: "모델 호출",
|
|
64
|
+
sessions: "세션",
|
|
65
|
+
daily: "일별 토큰",
|
|
66
|
+
recent: "일별 토큰 · 최근 30일",
|
|
67
|
+
models: "제공자 / 모델별",
|
|
68
|
+
unknown: "알 수 없음",
|
|
69
|
+
updated: "갱신 시각",
|
|
70
|
+
unreported: "토큰 수가 보고되지 않은 호출",
|
|
71
|
+
unreadable: "읽지 못한 세션",
|
|
72
|
+
scope: "서브에이전트와 재시도를 포함하여 세션에 기록된 호출만 집계합니다. 분기한 세션의 복제 기록은 한 번만 집계합니다. 삭제된 세션과 제목 생성 등 세션 외부 호출은 제외하며, 청구 금액을 나타내지는 않습니다.",
|
|
73
|
+
accounting: "입력에는 캐시 읽기·쓰기를 포함하지 않습니다. 추론 토큰은 출력에 포함합니다. 제공자가 전체 토큰 수를 보고하면 해당 값을 사용합니다."
|
|
74
|
+
},
|
|
75
|
+
zh: {
|
|
76
|
+
nav: "用量",
|
|
77
|
+
period: "时间范围",
|
|
78
|
+
week: "最近 7 天",
|
|
79
|
+
month: "最近 30 天",
|
|
80
|
+
all: "全部时间",
|
|
81
|
+
refresh: "刷新",
|
|
82
|
+
loading: "正在加载用量…",
|
|
83
|
+
error: "无法加载用量,请刷新重试。",
|
|
84
|
+
empty: "此时间范围内没有记录的模型调用。",
|
|
85
|
+
total: "总令牌数",
|
|
86
|
+
input: "未缓存输入",
|
|
87
|
+
output: "输出",
|
|
88
|
+
cacheRead: "缓存读取",
|
|
89
|
+
cacheWrite: "缓存写入",
|
|
90
|
+
calls: "模型调用",
|
|
91
|
+
sessions: "会话",
|
|
92
|
+
daily: "每日令牌数",
|
|
93
|
+
recent: "每日令牌数 · 最近 30 天",
|
|
94
|
+
models: "按提供方 / 模型",
|
|
95
|
+
unknown: "未知",
|
|
96
|
+
updated: "更新时间",
|
|
97
|
+
unreported: "未报告令牌数的调用",
|
|
98
|
+
unreadable: "无法读取的会话",
|
|
99
|
+
scope: "仅统计会话中记录的调用,包括子代理和重试。分支继承的历史仅统计一次。不包括已删除的会话或标题生成等会话外调用。这不是账单。",
|
|
100
|
+
accounting: "输入不含缓存读取和写入,推理令牌已包含在输出中。如提供方报告总令牌数,则使用该值。"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const localizedCopy = (language) => copy[language.startsWith("ko") ? "ko" : language.startsWith("zh") ? "zh" : "en"];
|
|
104
|
+
// src/protocol.ts
|
|
105
|
+
const channel = "/api";
|
|
106
|
+
const method = "dungsil-usage/describe";
|
|
107
|
+
// src/client.tsx
|
|
108
|
+
const name = "dungsil-ui-usage";
|
|
109
|
+
const inject = [
|
|
110
|
+
"connection",
|
|
111
|
+
"locale",
|
|
112
|
+
"slots"
|
|
113
|
+
];
|
|
114
|
+
const styles = `
|
|
115
|
+
.dsh-usage-page { box-sizing: border-box; height: 100%; overflow: auto; padding: 24px; padding-inline-start: max(24px, var(--dsh-frame-leading-clearance, 0px)); }
|
|
116
|
+
.dsh-usage-page > .dsh-usage { max-width: 960px; margin: 0 auto; }
|
|
117
|
+
.dsh-usage h1 { margin: 0 0 24px; font-size: 20px; line-height: 28px; font-weight: 500; }
|
|
118
|
+
.dsh-usage { container-type: inline-size; overflow-wrap: anywhere; color: var(--dsw-alias-label-primary); font: 14px/22px var(--dsw-font-family, system-ui); }
|
|
119
|
+
.dsh-usage-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
120
|
+
.dsh-usage-status { display: flex; align-items: center; gap: 6px; }
|
|
121
|
+
.dsh-usage-muted, .dsh-usage dt { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
122
|
+
.dsh-usage h3 { margin: 24px 0 12px; font-size: 14px; font-weight: 500; }
|
|
123
|
+
.dsh-usage dl { margin: 0; }
|
|
124
|
+
.dsh-usage dd { margin: 4px 0 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
125
|
+
.dsh-usage-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; padding: 20px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
126
|
+
.dsh-usage-summary > div:first-child { grid-column: 1 / -1; }
|
|
127
|
+
.dsh-usage-total { font-size: 28px; line-height: 36px; }
|
|
128
|
+
.dsh-usage-meta { display: flex; flex-wrap: wrap; gap: 8px 24px; margin: 12px 0; }
|
|
129
|
+
.dsh-usage-chart { display: flex; align-items: stretch; gap: clamp(1px, .5cqi, 4px); height: 120px; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
130
|
+
.dsh-usage-bar { flex: 1; min-width: 0; display: flex; align-items: flex-end; border: none; padding: 0; background: transparent; cursor: pointer; }
|
|
131
|
+
.dsh-usage-bar span { width: 100%; min-height: 2px; border-radius: 3px 3px 0 0; background: var(--dsw-alias-brand-primary-new-colorprimary-new-color); opacity: .55; }
|
|
132
|
+
.dsh-usage-bar[aria-pressed=true] span, .dsh-usage-bar:hover span { opacity: 1; }
|
|
133
|
+
.dsh-usage-bar:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
|
|
134
|
+
.dsh-usage-axis { display: flex; justify-content: space-between; gap: 12px; margin-top: 4px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
135
|
+
.dsh-usage-detail { min-height: 22px; margin: 8px 0; font-variant-numeric: tabular-nums; }
|
|
136
|
+
.dsh-usage-model { padding: 14px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
137
|
+
.dsh-usage-model-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; overflow-wrap: anywhere; }
|
|
138
|
+
.dsh-usage-model-heading > span:first-child { min-width: 0; }
|
|
139
|
+
.dsh-usage-model-heading > span:last-child { flex: none; font-variant-numeric: tabular-nums; }
|
|
140
|
+
.dsh-usage-model dl { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-top: 12px; font-size: 12px; }
|
|
141
|
+
.dsh-usage [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
|
|
142
|
+
@container (max-width: 480px) {
|
|
143
|
+
.dsh-usage-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
144
|
+
.dsh-usage-model dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
145
|
+
}
|
|
146
|
+
@container (max-width: 260px) {
|
|
147
|
+
.dsh-usage-summary, .dsh-usage-model dl { grid-template-columns: minmax(0, 1fr); }
|
|
148
|
+
.dsh-usage-model-heading { flex-direction: column; gap: 4px; }
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
function Usage({ ctx }) {
|
|
152
|
+
const locale = useLocaleSnapshot(ctx.locale);
|
|
153
|
+
const text = localizedCopy(locale.active);
|
|
154
|
+
const number = new Intl.NumberFormat(locale.active);
|
|
155
|
+
const [period, setPeriod] = (0, react.useState)(7);
|
|
156
|
+
const [periodOpen, setPeriodOpen] = (0, react.useState)(false);
|
|
157
|
+
const [refresh, setRefresh] = (0, react.useState)(0);
|
|
158
|
+
const [view, setView] = (0, react.useState)();
|
|
159
|
+
const [busy, setBusy] = (0, react.useState)(true);
|
|
160
|
+
const [error, setError] = (0, react.useState)(false);
|
|
161
|
+
const [selectedDay, setSelectedDay] = (0, react.useState)("");
|
|
162
|
+
const connection = ctx.get("connection");
|
|
163
|
+
(0, react.useEffect)(() => {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
setBusy(true);
|
|
166
|
+
setError(false);
|
|
167
|
+
setView(void 0);
|
|
168
|
+
(async () => {
|
|
169
|
+
try {
|
|
170
|
+
const result = await connection.rpc.call(channel, method, {
|
|
171
|
+
days: period,
|
|
172
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
173
|
+
}, controller.signal);
|
|
174
|
+
if (controller.signal.aborted) return;
|
|
175
|
+
if (!result.ok) throw new Error("Usage unavailable");
|
|
176
|
+
const next = result.value;
|
|
177
|
+
setView(next);
|
|
178
|
+
setSelectedDay(next.days.at(-1)?.day ?? "");
|
|
179
|
+
} catch {
|
|
180
|
+
if (!controller.signal.aborted) setError(true);
|
|
181
|
+
} finally {
|
|
182
|
+
if (!controller.signal.aborted) setBusy(false);
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
return () => controller.abort();
|
|
186
|
+
}, [
|
|
187
|
+
connection,
|
|
188
|
+
period,
|
|
189
|
+
refresh
|
|
190
|
+
]);
|
|
191
|
+
const metrics = [
|
|
192
|
+
"input",
|
|
193
|
+
"output",
|
|
194
|
+
"cacheRead",
|
|
195
|
+
"cacheWrite"
|
|
196
|
+
];
|
|
197
|
+
const periods = [
|
|
198
|
+
{
|
|
199
|
+
id: "7",
|
|
200
|
+
label: text.week
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "30",
|
|
204
|
+
label: text.month
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "0",
|
|
208
|
+
label: text.all
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
const selected = view?.days.find(({ day }) => day === selectedDay);
|
|
212
|
+
const maximum = Math.max(1, ...view?.days.map(({ total }) => total) ?? []);
|
|
213
|
+
const date = (day) => new Intl.DateTimeFormat(locale.active, {
|
|
214
|
+
month: "short",
|
|
215
|
+
day: "numeric",
|
|
216
|
+
timeZone: "UTC"
|
|
217
|
+
}).format(/* @__PURE__ */ new Date(`${day}T00:00:00Z`));
|
|
218
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
219
|
+
className: "dsh-usage",
|
|
220
|
+
"aria-label": text.nav,
|
|
221
|
+
"aria-busy": busy,
|
|
222
|
+
children: [
|
|
223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: text.nav }),
|
|
224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
225
|
+
className: "dsh-usage-toolbar",
|
|
226
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
227
|
+
open: periodOpen,
|
|
228
|
+
onClose: () => setPeriodOpen(false),
|
|
229
|
+
selectedId: String(period),
|
|
230
|
+
items: periods,
|
|
231
|
+
onSelect: (id) => {
|
|
232
|
+
setPeriodOpen(false);
|
|
233
|
+
setPeriod(Number(id));
|
|
234
|
+
},
|
|
235
|
+
portal: true,
|
|
236
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
237
|
+
variant: "outline",
|
|
238
|
+
"aria-label": text.period,
|
|
239
|
+
"aria-haspopup": "menu",
|
|
240
|
+
"aria-expanded": periodOpen,
|
|
241
|
+
onClick: () => setPeriodOpen((open) => !open),
|
|
242
|
+
children: [periods.find(({ id }) => id === String(period)).label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})]
|
|
243
|
+
})
|
|
244
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
245
|
+
variant: "outline",
|
|
246
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
|
|
247
|
+
disabled: busy,
|
|
248
|
+
onClick: () => setRefresh((value) => value + 1),
|
|
249
|
+
children: text.refresh
|
|
250
|
+
})]
|
|
251
|
+
}),
|
|
252
|
+
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
253
|
+
role: "status",
|
|
254
|
+
className: "dsh-usage-status",
|
|
255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }), text.loading]
|
|
256
|
+
}),
|
|
257
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
258
|
+
role: "alert",
|
|
259
|
+
children: text.error
|
|
260
|
+
}),
|
|
261
|
+
view && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
263
|
+
className: "dsh-usage-summary",
|
|
264
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.total }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
265
|
+
className: "dsh-usage-total",
|
|
266
|
+
children: number.format(view.totals.total)
|
|
267
|
+
})] }), metrics.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: number.format(view.totals[key]) })] }, key))]
|
|
268
|
+
}),
|
|
269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
270
|
+
className: "dsh-usage-meta dsh-usage-muted",
|
|
271
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
272
|
+
text.calls,
|
|
273
|
+
": ",
|
|
274
|
+
number.format(view.totals.calls)
|
|
275
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
276
|
+
text.sessions,
|
|
277
|
+
": ",
|
|
278
|
+
number.format(view.sessions)
|
|
279
|
+
] })]
|
|
280
|
+
}),
|
|
281
|
+
view.unreadable > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
282
|
+
role: "alert",
|
|
283
|
+
children: [
|
|
284
|
+
text.unreadable,
|
|
285
|
+
": ",
|
|
286
|
+
number.format(view.unreadable)
|
|
287
|
+
]
|
|
288
|
+
}),
|
|
289
|
+
view.totals.unreported > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
290
|
+
role: "status",
|
|
291
|
+
children: [
|
|
292
|
+
text.unreported,
|
|
293
|
+
": ",
|
|
294
|
+
number.format(view.totals.unreported)
|
|
295
|
+
]
|
|
296
|
+
}),
|
|
297
|
+
view.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
298
|
+
role: "status",
|
|
299
|
+
children: text.empty
|
|
300
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: period === 0 ? text.recent : text.daily }),
|
|
302
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
303
|
+
className: "dsh-usage-chart",
|
|
304
|
+
role: "group",
|
|
305
|
+
"aria-label": `${text.daily} (${view.timeZone})`,
|
|
306
|
+
children: view.days.map(({ day, total }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
307
|
+
type: "button",
|
|
308
|
+
className: "dsh-usage-bar",
|
|
309
|
+
"aria-label": `${day}: ${number.format(total)} ${text.total}`,
|
|
310
|
+
"aria-pressed": day === selectedDay,
|
|
311
|
+
onClick: () => setSelectedDay(day),
|
|
312
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: `${total / maximum * 100}%` } })
|
|
313
|
+
}, day))
|
|
314
|
+
}),
|
|
315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
316
|
+
className: "dsh-usage-axis",
|
|
317
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: date(view.days[0].day) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: date(view.days.at(-1).day) })]
|
|
318
|
+
}),
|
|
319
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
320
|
+
className: "dsh-usage-detail",
|
|
321
|
+
"aria-live": "polite",
|
|
322
|
+
children: selected && `${date(selected.day)} · ${text.total}: ${number.format(selected.total)}`
|
|
323
|
+
}),
|
|
324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.models }),
|
|
325
|
+
view.models.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
326
|
+
className: "dsh-usage-model",
|
|
327
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
328
|
+
className: "dsh-usage-model-heading",
|
|
329
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
330
|
+
row.provider || text.unknown,
|
|
331
|
+
" / ",
|
|
332
|
+
row.model || text.unknown
|
|
333
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: number.format(row.total) })]
|
|
334
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["calls", ...metrics].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: number.format(row[key]) })] }, key)) })]
|
|
335
|
+
}, JSON.stringify([row.provider, row.model])))
|
|
336
|
+
] }),
|
|
337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
338
|
+
className: "dsh-usage-muted",
|
|
339
|
+
children: [
|
|
340
|
+
text.updated,
|
|
341
|
+
":",
|
|
342
|
+
" ",
|
|
343
|
+
new Intl.DateTimeFormat(locale.active, {
|
|
344
|
+
dateStyle: "medium",
|
|
345
|
+
timeStyle: "short",
|
|
346
|
+
timeZone: view.timeZone
|
|
347
|
+
}).format(view.updatedAt),
|
|
348
|
+
" ",
|
|
349
|
+
"(",
|
|
350
|
+
view.timeZone,
|
|
351
|
+
")"
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
356
|
+
className: "dsh-usage-muted",
|
|
357
|
+
children: text.scope
|
|
358
|
+
}),
|
|
359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
360
|
+
className: "dsh-usage-muted",
|
|
361
|
+
children: text.accounting
|
|
362
|
+
})
|
|
363
|
+
]
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function apply(ctx) {
|
|
367
|
+
ctx.effect(() => installStyles(styles), "usage: styles");
|
|
368
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
369
|
+
name: "main",
|
|
370
|
+
key: name,
|
|
371
|
+
inject: () => ({ ctx })
|
|
372
|
+
}, ({ ctx }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
373
|
+
className: "dsh-usage-page",
|
|
374
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Usage, { ctx })
|
|
375
|
+
})));
|
|
376
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
377
|
+
name: "sidebar.panellist",
|
|
378
|
+
id: name,
|
|
379
|
+
order: 1,
|
|
380
|
+
label: () => localizedCopy(ctx.locale.getSnapshot().active).nav
|
|
381
|
+
}, ({ size }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutlineRegular, { size })));
|
|
382
|
+
}
|
|
383
|
+
exports.apply = apply;
|
|
384
|
+
exports.inject = inject;
|
|
385
|
+
exports.name = name;
|
|
386
|
+
|
|
387
|
+
return module.exports; } });
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
2
|
+
import { lastAssistantStreamChunk } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
// src/protocol.ts
|
|
4
|
+
const method = "dungsil-usage/describe";
|
|
5
|
+
// src/index.ts
|
|
6
|
+
const name = "dungsil-ui-usage";
|
|
7
|
+
const inject = ["connection", "sessionQuery"];
|
|
8
|
+
const empty = () => ({
|
|
9
|
+
calls: 0,
|
|
10
|
+
unreported: 0,
|
|
11
|
+
input: 0,
|
|
12
|
+
output: 0,
|
|
13
|
+
cacheRead: 0,
|
|
14
|
+
cacheWrite: 0,
|
|
15
|
+
total: 0
|
|
16
|
+
});
|
|
17
|
+
function add(totals, usage) {
|
|
18
|
+
totals.calls++;
|
|
19
|
+
if (!usage) {
|
|
20
|
+
totals.unreported++;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
totals.input += usage.inputTokens;
|
|
24
|
+
totals.output += usage.outputTokens;
|
|
25
|
+
totals.cacheRead += usage.cacheReadTokens ?? 0;
|
|
26
|
+
totals.cacheWrite += usage.cacheWriteTokens ?? 0;
|
|
27
|
+
totals.total += usage.totalTokens ?? usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
28
|
+
}
|
|
29
|
+
function apply(ctx) {
|
|
30
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
31
|
+
path: `/api/${method}`,
|
|
32
|
+
methods: ["POST"],
|
|
33
|
+
requestBody: "buffered",
|
|
34
|
+
async fetch(request) {
|
|
35
|
+
let rpcId = "";
|
|
36
|
+
const failure = (code) => ({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: {
|
|
39
|
+
code,
|
|
40
|
+
message: "Usage unavailable",
|
|
41
|
+
details: {}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
const body = clientRequestSchema.parse(await request.json());
|
|
47
|
+
rpcId = body.rpcId;
|
|
48
|
+
const input = body.payload;
|
|
49
|
+
if (body.method !== "dungsil-usage/describe" || !input || ![
|
|
50
|
+
7,
|
|
51
|
+
30,
|
|
52
|
+
0
|
|
53
|
+
].includes(input.days) || typeof input.timeZone !== "string") return Response.json({
|
|
54
|
+
type: "server-response",
|
|
55
|
+
rpcId,
|
|
56
|
+
result: failure("invalid")
|
|
57
|
+
}, { headers: { "Cache-Control": "no-store" } });
|
|
58
|
+
let formatter;
|
|
59
|
+
try {
|
|
60
|
+
formatter = new Intl.DateTimeFormat("en-CA", {
|
|
61
|
+
timeZone: input.timeZone,
|
|
62
|
+
year: "numeric",
|
|
63
|
+
month: "2-digit",
|
|
64
|
+
day: "2-digit"
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
return Response.json({
|
|
68
|
+
type: "server-response",
|
|
69
|
+
rpcId,
|
|
70
|
+
result: failure("invalid")
|
|
71
|
+
}, { headers: { "Cache-Control": "no-store" } });
|
|
72
|
+
}
|
|
73
|
+
const signal = AbortSignal.any([request.signal, AbortSignal.timeout(6e4)]);
|
|
74
|
+
const updatedAt = Date.now();
|
|
75
|
+
const today = formatter.format(updatedAt);
|
|
76
|
+
const start = input.days === 0 ? "" : (/* @__PURE__ */ new Date(Date.parse(`${today}T00:00:00Z`) - (input.days - 1) * 864e5)).toISOString().slice(0, 10);
|
|
77
|
+
const view = {
|
|
78
|
+
totals: empty(),
|
|
79
|
+
models: [],
|
|
80
|
+
days: [],
|
|
81
|
+
sessions: 0,
|
|
82
|
+
unreadable: 0,
|
|
83
|
+
updatedAt,
|
|
84
|
+
timeZone: input.timeZone
|
|
85
|
+
};
|
|
86
|
+
const models = /* @__PURE__ */ new Map();
|
|
87
|
+
const days = /* @__PURE__ */ new Map();
|
|
88
|
+
const records = await ctx.sessionQuery.listSessions(signal);
|
|
89
|
+
let index = 0;
|
|
90
|
+
await Promise.all(Array.from({ length: Math.min(4, records.length) }, async () => {
|
|
91
|
+
while (index < records.length) {
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
const record = records[index++];
|
|
94
|
+
try {
|
|
95
|
+
using observation = await ctx.sessionQuery.observeSession(record.header.id, {
|
|
96
|
+
signal,
|
|
97
|
+
projectionMode: "none"
|
|
98
|
+
});
|
|
99
|
+
let provider = "";
|
|
100
|
+
let model = "";
|
|
101
|
+
let included = false;
|
|
102
|
+
for (const event of observation.events) {
|
|
103
|
+
if (event.type === "request/context") ({provider, model} = event.data);
|
|
104
|
+
if (event.type === "request/header") ({provider, model} = event.data.header.config);
|
|
105
|
+
if (event.seq < observation.inheritedEventCount || event.time > updatedAt) continue;
|
|
106
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
107
|
+
const day = formatter.format(event.time);
|
|
108
|
+
if (day < start || day > today) continue;
|
|
109
|
+
const usage = event.type === "assistant/message" ? event.data.usage : lastAssistantStreamChunk(event.data.stream, "usage")?.usage;
|
|
110
|
+
const route = event.type === "assistant/message" ? event.data.message.source : {
|
|
111
|
+
provider,
|
|
112
|
+
model
|
|
113
|
+
};
|
|
114
|
+
const key = JSON.stringify([route.provider, route.model]);
|
|
115
|
+
let row = models.get(key);
|
|
116
|
+
if (!row) models.set(key, row = {
|
|
117
|
+
provider: route.provider,
|
|
118
|
+
model: route.model,
|
|
119
|
+
...empty()
|
|
120
|
+
});
|
|
121
|
+
const before = row.total;
|
|
122
|
+
add(row, usage);
|
|
123
|
+
add(view.totals, usage);
|
|
124
|
+
days.set(day, (days.get(day) ?? 0) + row.total - before);
|
|
125
|
+
included = true;
|
|
126
|
+
}
|
|
127
|
+
if (included) view.sessions++;
|
|
128
|
+
} catch {
|
|
129
|
+
signal.throwIfAborted();
|
|
130
|
+
view.unreadable++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
view.models = [...models.values()].sort((a, b) => b.total - a.total || a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model));
|
|
135
|
+
const chartStart = start || (/* @__PURE__ */ new Date(Date.parse(`${today}T00:00:00Z`) - 25056e5)).toISOString().slice(0, 10);
|
|
136
|
+
for (let date = Date.parse(`${chartStart}T00:00:00Z`); date <= Date.parse(`${today}T00:00:00Z`); date += 864e5) {
|
|
137
|
+
const day = new Date(date).toISOString().slice(0, 10);
|
|
138
|
+
view.days.push({
|
|
139
|
+
day,
|
|
140
|
+
total: days.get(day) ?? 0
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
result = {
|
|
144
|
+
ok: true,
|
|
145
|
+
value: view
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
result = failure("unavailable");
|
|
149
|
+
}
|
|
150
|
+
return Response.json({
|
|
151
|
+
type: "server-response",
|
|
152
|
+
rpcId,
|
|
153
|
+
result
|
|
154
|
+
}, { headers: { "Cache-Control": "no-store" } });
|
|
155
|
+
}
|
|
156
|
+
}), "usage: owner-only session accounting");
|
|
157
|
+
}
|
|
158
|
+
export { apply, inject, name };
|
package/locale/en.json
ADDED
package/locale/ko.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungsil/dsh-plugin-ui-usage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Recorded session token usage dashboard for DeepSeek Harness.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/dungsil/dsh-plugins.git",
|
|
9
|
+
"directory": "packages/ui-usage"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"locale",
|
|
14
|
+
"cordis.patch.yml"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./client": "./dist/client.js",
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./locale/*.json": "./locale/*.json"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
28
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
|
|
29
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.5",
|
|
31
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.5",
|
|
33
|
+
"@deepseek-ai/dsh-credentials-local": "0.2.1-rc.5",
|
|
34
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.5",
|
|
35
|
+
"@deepseek-ai/dsh-session-query": "0.2.1-rc.5",
|
|
36
|
+
"@dungsil/dsh-tsconfig": "0.0.0",
|
|
37
|
+
"@dungsil/dsh-tsdown": "0.0.0",
|
|
38
|
+
"@dungsil/dsh-ui": "0.0.0",
|
|
39
|
+
"@types/react": "18.3.31",
|
|
40
|
+
"react": "18.3.1",
|
|
41
|
+
"tsdown": "0.23.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.5",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.5",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.5",
|
|
47
|
+
"@deepseek-ai/dsh-session-query": "^0.2.1-rc.5"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"dsh": "^0.2.1-rc.5",
|
|
51
|
+
"node": ">=24.9.0"
|
|
52
|
+
},
|
|
53
|
+
"dsh": {
|
|
54
|
+
"bundle": {
|
|
55
|
+
"patch": "./cordis.patch.yml"
|
|
56
|
+
},
|
|
57
|
+
"client": {
|
|
58
|
+
"platform": "web",
|
|
59
|
+
"immediately": true,
|
|
60
|
+
"inject": [
|
|
61
|
+
"@deepseek-ai/dsh-client-connection",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"market": {
|
|
70
|
+
"order": 12,
|
|
71
|
+
"title": {
|
|
72
|
+
"en": "Usage Dashboard",
|
|
73
|
+
"ko": "사용량 대시보드",
|
|
74
|
+
"zh": "用量仪表盘"
|
|
75
|
+
},
|
|
76
|
+
"description": {
|
|
77
|
+
"en": "View recorded session tokens, daily activity, and model usage from the sidebar.",
|
|
78
|
+
"ko": "사이드바에서 세션에 기록된 토큰, 일별 추이, 모델별 사용량을 확인합니다.",
|
|
79
|
+
"zh": "从侧边栏查看会话记录的令牌、每日趋势和模型用量。"
|
|
80
|
+
},
|
|
81
|
+
"category": "ui",
|
|
82
|
+
"labels": [],
|
|
83
|
+
"keywords": [
|
|
84
|
+
"usage",
|
|
85
|
+
"tokens",
|
|
86
|
+
"dashboard",
|
|
87
|
+
"models"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"build": "tsdown",
|
|
92
|
+
"typecheck": "tsc --noEmit",
|
|
93
|
+
"test": "node --test test/*.test.mjs"
|
|
94
|
+
}
|
|
6
95
|
}
|
package/README.md
DELETED