@cassius0924/dsh-usage-dashboard 0.3.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/lib/index.js ADDED
@@ -0,0 +1,433 @@
1
+ // src/memo.ts
2
+ function memoize(ttlMs, compute) {
3
+ const state = { entry: void 0, inflight: void 0 };
4
+ const run = () => {
5
+ if (state.inflight !== void 0) return state.inflight;
6
+ const task = compute().then(
7
+ (value) => {
8
+ state.inflight = void 0;
9
+ if (value.ok) state.entry = { value, setAt: Date.now() };
10
+ return value;
11
+ },
12
+ (err) => {
13
+ state.inflight = void 0;
14
+ throw err;
15
+ }
16
+ );
17
+ state.inflight = task;
18
+ return task;
19
+ };
20
+ return {
21
+ get: () => {
22
+ const hit = state.entry;
23
+ if (hit !== void 0 && hit.value.ok && Date.now() - hit.setAt < ttlMs) {
24
+ return Promise.resolve(hit.value);
25
+ }
26
+ return run();
27
+ },
28
+ refresh: () => run()
29
+ };
30
+ }
31
+
32
+ // src/trust-fence.ts
33
+ function isLoopbackHostname(hostname) {
34
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
35
+ }
36
+ function isTrustedApiRequest(req) {
37
+ const host = req.headers.host;
38
+ if (host === void 0) return false;
39
+ let hostUrl;
40
+ try {
41
+ hostUrl = new URL(`http://${host}`);
42
+ } catch {
43
+ return false;
44
+ }
45
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
46
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
47
+ const origin = req.headers.origin;
48
+ if (origin === void 0) return true;
49
+ try {
50
+ return new URL(origin).host === hostUrl.host;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ // src/pricing.ts
57
+ var PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
58
+ var PEAK_WINDOWS = [[9, 12], [14, 18]];
59
+ var BEIJING_OFFSET_MS = 8 * 36e5;
60
+ var LEGACY_RATES = {
61
+ pro: { cacheHit: 0.025, input: 3, output: 6 },
62
+ flash: { cacheHit: 0.02, input: 1, output: 2 }
63
+ };
64
+ var PEAK_RATES = {
65
+ pro: { cacheHit: 0.3, input: 9, output: 27 },
66
+ flash: { cacheHit: 0.1, input: 3, output: 9 }
67
+ };
68
+ var halved = (rates) => ({
69
+ cacheHit: rates.cacheHit / 2,
70
+ input: rates.input / 2,
71
+ output: rates.output / 2
72
+ });
73
+ function tierOf(model) {
74
+ return model.toLowerCase().includes("flash") ? "flash" : "pro";
75
+ }
76
+ function isPeak(timeMs) {
77
+ const hour = Math.floor(((timeMs + BEIJING_OFFSET_MS) % 864e5 + 864e5) % 864e5 / 36e5);
78
+ return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to);
79
+ }
80
+ function ratesAt(timeMs, model) {
81
+ const tier = tierOf(model);
82
+ if (timeMs < PEAK_PRICING_FROM_MS) return LEGACY_RATES[tier];
83
+ const peak = PEAK_RATES[tier];
84
+ return isPeak(timeMs) ? peak : halved(peak);
85
+ }
86
+ function cacheSavingOf(timeMs, model, cache) {
87
+ const rates = ratesAt(timeMs, model);
88
+ return cache * (rates.input - rates.cacheHit) / 1e6;
89
+ }
90
+ var applyRates = (rates, input, cache, output) => (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1e6;
91
+ function costOf(timeMs, model, input, cache, output) {
92
+ return applyRates(ratesAt(timeMs, model), input, cache, output);
93
+ }
94
+ function costUnderPeakEra(timeMs, model, input, cache, output, forceOffPeak = false) {
95
+ const peak = PEAK_RATES[tierOf(model)];
96
+ const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak);
97
+ return applyRates(rates, input, cache, output);
98
+ }
99
+ function pricingInfo(nowMs) {
100
+ const split = nowMs >= PEAK_PRICING_FROM_MS;
101
+ return {
102
+ currency: "CNY",
103
+ switchDate: "2026-08-17",
104
+ splitActive: split,
105
+ inPeakNow: split && isPeak(nowMs),
106
+ peakWindows: ["09:00\u201312:00", "14:00\u201318:00"],
107
+ tiers: ["pro", "flash"].map((tier) => ({
108
+ model: tier === "pro" ? "deepseek-v4-pro" : "deepseek-v4-flash",
109
+ peak: split ? PEAK_RATES[tier] : LEGACY_RATES[tier],
110
+ offPeak: split ? halved(PEAK_RATES[tier]) : null
111
+ }))
112
+ };
113
+ }
114
+
115
+ // src/usage.ts
116
+ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
117
+ function errorMessage(err) {
118
+ return err?.message ?? String(err);
119
+ }
120
+ async function fetchBalance(credentials) {
121
+ if (credentials === void 0) return { ok: false, error: "\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528" };
122
+ let cred;
123
+ try {
124
+ cred = await credentials.resolve("DEEPSEEK_API_KEY");
125
+ } catch (err) {
126
+ return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` };
127
+ }
128
+ if (cred === void 0 || cred.value === "") {
129
+ return { ok: false, error: "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09" };
130
+ }
131
+ let res;
132
+ try {
133
+ res = await fetch("https://api.deepseek.com/user/balance", {
134
+ headers: { authorization: `Bearer ${cred.value}` },
135
+ signal: AbortSignal.timeout(2e4)
136
+ });
137
+ } catch (err) {
138
+ return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` };
139
+ }
140
+ if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` };
141
+ let parsed;
142
+ try {
143
+ parsed = await res.json();
144
+ } catch {
145
+ return { ok: false, error: "\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25" };
146
+ }
147
+ const p = parsed;
148
+ return {
149
+ ok: true,
150
+ data: {
151
+ isAvailable: p.is_available === true,
152
+ balances: (p.balance_infos ?? []).map((b) => ({
153
+ currency: b.currency ?? "",
154
+ total: b.total_balance ?? "0",
155
+ granted: b.granted_balance ?? "0",
156
+ toppedUp: b.topped_up_balance ?? "0"
157
+ }))
158
+ }
159
+ };
160
+ }
161
+ var emptyBucket = () => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 });
162
+ var SESSION_TOP_N = 6;
163
+ function titleOf(events, id) {
164
+ let title = "";
165
+ if (events !== void 0) {
166
+ for (const ev of events) {
167
+ if (ev?.type !== "session/title") continue;
168
+ const next = ev.data?.title;
169
+ if (typeof next === "string" && next !== "") title = next;
170
+ }
171
+ }
172
+ return title !== "" ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`;
173
+ }
174
+ var dayKeyOf = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
175
+ function summarize(dayMap, now) {
176
+ const sum = (matches) => {
177
+ const acc = { total: 0, cost: 0, calls: 0 };
178
+ for (const [key, bucket] of dayMap) {
179
+ if (!matches(key)) continue;
180
+ acc.total += bucket.total;
181
+ acc.cost += bucket.cost;
182
+ acc.calls += bucket.calls;
183
+ }
184
+ return acc;
185
+ };
186
+ const todayKey = dayKeyOf(now);
187
+ const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1));
188
+ const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`;
189
+ const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
190
+ const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`;
191
+ const dayOfMonth = now.getDate();
192
+ return {
193
+ today: sum((key) => key === todayKey),
194
+ yesterday: sum((key) => key === yesterdayKey),
195
+ month: sum((key) => key.startsWith(monthPrefix)),
196
+ lastMonthToDate: sum((key) => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth)
197
+ };
198
+ }
199
+ function addUsageEvent(events, onEvent) {
200
+ if (events === void 0) return;
201
+ let provider = "";
202
+ let model = "";
203
+ for (const ev of events) {
204
+ if (ev?.type === "request/header") {
205
+ const cfg = ev.data?.header?.config;
206
+ if (cfg?.provider !== void 0 && cfg?.model !== void 0) {
207
+ provider = cfg.provider;
208
+ model = cfg.model;
209
+ }
210
+ continue;
211
+ }
212
+ if (ev?.type !== "assistant/message") continue;
213
+ const usage = ev.data?.usage;
214
+ if (usage === void 0) continue;
215
+ const input = Number(usage.inputTokens) || 0;
216
+ const output = Number(usage.outputTokens) || 0;
217
+ const cache = Number(usage.cacheReadTokens) || 0;
218
+ const reasoning = Number(usage.reasoningTokens) || 0;
219
+ if (ev.time === void 0) continue;
220
+ onEvent(ev.time, input, output, cache, reasoning, provider, model);
221
+ }
222
+ }
223
+ async function fetchUsage(persistence) {
224
+ if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
225
+ let headers;
226
+ try {
227
+ headers = await persistence.list();
228
+ } catch (err) {
229
+ return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` };
230
+ }
231
+ const dayMap = /* @__PURE__ */ new Map();
232
+ const hourMap = /* @__PURE__ */ new Map();
233
+ const overall = { ...emptyBucket(), reasoning: 0, cacheSavings: 0 };
234
+ const modelTotals = /* @__PURE__ */ new Map();
235
+ const modelDays = /* @__PURE__ */ new Map();
236
+ const modelHours = /* @__PURE__ */ new Map();
237
+ const sessions = [];
238
+ const peakSplit = {
239
+ peak: { total: 0, cost: 0, calls: 0 },
240
+ offPeak: { total: 0, cost: 0, calls: 0 },
241
+ peakEraCost: 0,
242
+ offPeakEraCost: 0
243
+ };
244
+ const bump = (map, key, input, output, cache, cost) => {
245
+ const bucket = map.get(key) ?? emptyBucket();
246
+ bucket.input += input;
247
+ bucket.output += output;
248
+ bucket.cache += cache;
249
+ bucket.total += input + output + cache;
250
+ bucket.cost += cost;
251
+ bucket.calls += 1;
252
+ map.set(key, bucket);
253
+ };
254
+ for (const header of headers) {
255
+ const sid = header.id ?? header.sessionId;
256
+ if (sid === void 0 || sid === "") continue;
257
+ try {
258
+ const { events } = await persistence.readFrom(sid, 0);
259
+ const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
260
+ addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
261
+ const d = new Date(time);
262
+ const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
263
+ const hourKey = String(d.getHours());
264
+ const modelKey = `${provider}/${model}`;
265
+ const cost = costOf(time, model, input, cache, output);
266
+ bump(dayMap, dayKey, input, output, cache, cost);
267
+ bump(hourMap, hourKey, input, output, cache, cost);
268
+ bump(modelTotals, modelKey, input, output, cache, cost);
269
+ let days = modelDays.get(modelKey);
270
+ if (days === void 0) {
271
+ days = /* @__PURE__ */ new Map();
272
+ modelDays.set(modelKey, days);
273
+ }
274
+ bump(days, dayKey, input, output, cache, cost);
275
+ let hours = modelHours.get(modelKey);
276
+ if (hours === void 0) {
277
+ hours = /* @__PURE__ */ new Map();
278
+ modelHours.set(modelKey, hours);
279
+ }
280
+ bump(hours, hourKey, input, output, cache, cost);
281
+ overall.input += input;
282
+ overall.output += output;
283
+ overall.cache += cache;
284
+ overall.reasoning += reasoning;
285
+ overall.total += input + output + cache;
286
+ overall.cost += cost;
287
+ overall.cacheSavings += cacheSavingOf(time, model, cache);
288
+ session.total += input + output + cache;
289
+ session.cost += cost;
290
+ session.calls += 1;
291
+ if (time > session.lastActive) session.lastActive = time;
292
+ const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak;
293
+ window.total += input + output + cache;
294
+ window.cost += cost;
295
+ window.calls += 1;
296
+ peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output);
297
+ peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true);
298
+ overall.calls += 1;
299
+ });
300
+ if (session.calls > 0) {
301
+ session.title = titleOf(events, sid);
302
+ sessions.push(session);
303
+ }
304
+ } catch {
305
+ }
306
+ }
307
+ const daily = [];
308
+ for (let i = 29; i >= 0; i--) {
309
+ const d = new Date(Date.now() - i * 864e5);
310
+ const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
311
+ const b = dayMap.get(key) ?? emptyBucket();
312
+ daily.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls });
313
+ }
314
+ const hourly = [];
315
+ for (let i = 0; i < 24; i++) {
316
+ const b = hourMap.get(String(i)) ?? emptyBucket();
317
+ hourly.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
318
+ }
319
+ const heatmap = [];
320
+ for (let i = 83; i >= 0; i--) {
321
+ const d = new Date(Date.now() - i * 864e5);
322
+ const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
323
+ const b = dayMap.get(key) ?? emptyBucket();
324
+ heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
325
+ }
326
+ const models = [];
327
+ for (const [modelKey, bucket] of modelTotals) {
328
+ const slash = modelKey.indexOf("/");
329
+ const provider = slash < 0 ? "" : modelKey.slice(0, slash);
330
+ const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
331
+ const days = modelDays.get(modelKey);
332
+ const hours = modelHours.get(modelKey);
333
+ const daily2 = [];
334
+ for (let i = 29; i >= 0; i--) {
335
+ const d = new Date(Date.now() - i * 864e5);
336
+ const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
337
+ const b = days?.get(key) ?? emptyBucket();
338
+ daily2.push({ total: b.total, cost: b.cost, calls: b.calls });
339
+ }
340
+ const hourly2 = [];
341
+ for (let i = 0; i < 24; i++) {
342
+ const b = hours?.get(String(i)) ?? emptyBucket();
343
+ hourly2.push({ total: b.total, cost: b.cost, calls: b.calls });
344
+ }
345
+ models.push({
346
+ provider,
347
+ model,
348
+ input: bucket.input,
349
+ output: bucket.output,
350
+ cache: bucket.cache,
351
+ total: bucket.total,
352
+ cost: bucket.cost,
353
+ calls: bucket.calls,
354
+ daily: daily2,
355
+ hourly: hourly2
356
+ });
357
+ }
358
+ models.sort((a, b) => b.total - a.total);
359
+ return {
360
+ ok: true,
361
+ data: {
362
+ daily,
363
+ hourly,
364
+ heatmap,
365
+ models,
366
+ summary: summarize(dayMap, /* @__PURE__ */ new Date()),
367
+ pricing: pricingInfo(Date.now()),
368
+ peakSplit,
369
+ sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
370
+ sessionCount: sessions.length,
371
+ totals: {
372
+ input: overall.input,
373
+ output: overall.output,
374
+ cache: overall.cache,
375
+ reasoning: overall.reasoning,
376
+ total: overall.total,
377
+ cost: overall.cost,
378
+ calls: overall.calls,
379
+ cacheSavings: overall.cacheSavings
380
+ }
381
+ }
382
+ };
383
+ }
384
+
385
+ // src/wire.ts
386
+ function writeJson(res, status, body, headers) {
387
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
388
+ res.end(JSON.stringify(body));
389
+ }
390
+
391
+ // src/index.ts
392
+ var name = "dsh-usage-dashboard";
393
+ var inject = ["webServer", "credentials", "sessionPersistence"];
394
+ var BALANCE_TTL_MS = 6e4;
395
+ var USAGE_TTL_MS = 5 * 6e4;
396
+ function apply(ctx) {
397
+ const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials));
398
+ const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence));
399
+ ctx.effect(() => ctx.webServer.register({
400
+ kind: "prefix",
401
+ path: "/api/dsh-usage-dashboard",
402
+ handler: async (req, res) => {
403
+ if (!isTrustedApiRequest(req)) {
404
+ writeJson(res, 403, { ok: false, error: "forbidden" });
405
+ return;
406
+ }
407
+ if (req.method !== "GET") {
408
+ writeJson(res, 405, { ok: false, error: "method not allowed" });
409
+ return;
410
+ }
411
+ const url = new URL(req.url ?? "/", "http://dsh.internal");
412
+ const pathname = url.pathname;
413
+ const force = url.searchParams.get("refresh") === "1";
414
+ if (pathname === "/api/dsh-usage-dashboard/balance") {
415
+ const payload = await (force ? balanceMemo.refresh() : balanceMemo.get());
416
+ const headers = payload.ok ? { "cache-control": `private, max-age=${Math.round(BALANCE_TTL_MS / 1e3)}` } : { "cache-control": "no-store" };
417
+ writeJson(res, 200, payload, headers);
418
+ } else if (pathname === "/api/dsh-usage-dashboard/usage") {
419
+ const payload = await (force ? usageMemo.refresh() : usageMemo.get());
420
+ const headers = payload.ok ? { "cache-control": `private, max-age=${Math.round(USAGE_TTL_MS / 1e3)}` } : { "cache-control": "no-store" };
421
+ writeJson(res, 200, payload, headers);
422
+ } else {
423
+ writeJson(res, 404, { ok: false, error: "not found" });
424
+ }
425
+ }
426
+ }), "dsh-usage-dashboard: /api routes");
427
+ }
428
+ export {
429
+ apply,
430
+ inject,
431
+ name
432
+ };
433
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/memo.ts", "../src/trust-fence.ts", "../src/pricing.ts", "../src/usage.ts", "../src/wire.ts", "../src/index.ts"],
4
+ "sourcesContent": ["/**\n * Tiny TTL memo for the /api routes.\n *\n * The usage endpoint replays every session log, so it must not run on every\n * tab click; the balance endpoint also calls the DeepSeek API. The memo:\n *\n * - `get()` serves a successful cached response while it is within `ttlMs`,\n * otherwise recomputes;\n * - `refresh()` recomputes unconditionally and reseeds the cache;\n * - concurrent recomputes (of either kind) are deduped into one upstream\n * call;\n * - `ok:false` responses are never cached, so a transient failure is retried\n * on the next request.\n *\n * No timers are held, so nothing needs disposal on plugin unload.\n */\n\ninterface Entry<T> {\n value: T\n setAt: number\n}\n\ninterface MemoState<T> {\n entry: Entry<T> | undefined\n inflight: Promise<T> | undefined\n}\n\nexport interface Memo<T> {\n get(): Promise<T>\n refresh(): Promise<T>\n}\n\nexport function memoize<T extends { ok: boolean }>(ttlMs: number, compute: () => Promise<T>): Memo<T> {\n const state: MemoState<T> = { entry: undefined, inflight: undefined }\n\n const run = (): Promise<T> => {\n if (state.inflight !== undefined) return state.inflight\n const task = compute().then(\n (value) => {\n state.inflight = undefined\n if (value.ok) state.entry = { value, setAt: Date.now() }\n return value\n },\n (err: unknown) => {\n state.inflight = undefined\n throw err\n },\n )\n state.inflight = task\n return task\n }\n\n return {\n get: () => {\n const hit = state.entry\n if (hit !== undefined && hit.value.ok && Date.now() - hit.setAt < ttlMs) {\n return Promise.resolve(hit.value)\n }\n return run()\n },\n refresh: () => run(),\n }\n}\n", "/**\n * Browser-trust fence for the plugin's own /api route \u2014 the same fences the\n * harness's /api gateway applies: the Host header must name a loopback\n * authority, a browser-marked cross-site request is refused, and an attached\n * Origin must be exactly this authority.\n */\nimport type { IncomingMessage } from 'node:http'\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'\n}\n\nexport function isTrustedApiRequest(req: IncomingMessage): boolean {\n const host = req.headers.host\n if (host === undefined) return false\n let hostUrl: URL\n try {\n hostUrl = new URL(`http://${host}`)\n } catch {\n return false\n }\n if (!isLoopbackHostname(hostUrl.hostname)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === hostUrl.host\n } catch {\n return false\n }\n}\n", "/**\n * DeepSeek API price table and cost estimation.\n *\n * Two things the previous flat constant got wrong: every model was billed at\n * deepseek-v4-pro rates (flash is ~3x cheaper), and the 2026-08-17 switch to\n * peak/off-peak pricing was not modelled at all.\n *\n * Rates are CNY per 1M tokens, from DeepSeek's 2026-08-13 price announcement.\n * Peak window is Beijing time 09:00\u201312:00 and 14:00\u201318:00; off-peak is half of\n * peak. Usage recorded before the switch is still costed at the old flat rates,\n * so historical days keep the price that was actually charged.\n *\n * Editing this table is the one place to touch when DeepSeek changes prices.\n */\nimport type { PricingInfo, PricingRates } from './contract.ts'\n\n/** 2026-08-17 00:00 Beijing time (UTC+8), when peak/off-peak pricing starts. */\nexport const PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0)\n\n/** Peak hours in Beijing time, as [startHour, endHour) pairs. */\nconst PEAK_WINDOWS: Array<[number, number]> = [[9, 12], [14, 18]]\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\n\ntype Tier = 'pro' | 'flash'\n\n/** Flat rates in effect until 2026-08-17. */\nconst LEGACY_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.025, input: 3, output: 6 },\n flash: { cacheHit: 0.02, input: 1, output: 2 },\n}\n\n/** Peak rates from 2026-08-17; off-peak is exactly half of each. */\nconst PEAK_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.1, input: 3, output: 9 },\n}\n\nconst halved = (rates: PricingRates): PricingRates => ({\n cacheHit: rates.cacheHit / 2,\n input: rates.input / 2,\n output: rates.output / 2,\n})\n\n/** Which price tier a model name falls into; anything unrecognised is billed\n * as pro, the conservative (more expensive) guess. */\nexport function tierOf(model: string): Tier {\n return model.toLowerCase().includes('flash') ? 'flash' : 'pro'\n}\n\n/** Whether a moment falls in a peak window, judged in Beijing time so the\n * estimate does not drift with the machine's timezone. */\nexport function isPeak(timeMs: number): boolean {\n const hour = Math.floor((((timeMs + BEIJING_OFFSET_MS) % 86_400_000) + 86_400_000) % 86_400_000 / 3_600_000)\n return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to)\n}\n\n/** Rates applying to one model at one moment. */\nexport function ratesAt(timeMs: number, model: string): PricingRates {\n const tier = tierOf(model)\n if (timeMs < PEAK_PRICING_FROM_MS) return LEGACY_RATES[tier]\n const peak = PEAK_RATES[tier]\n return isPeak(timeMs) ? peak : halved(peak)\n}\n\n/**\n * What prefix caching saved on one record: the cache-hit tokens re-priced at\n * the cache-miss rate, minus what they actually cost.\n */\nexport function cacheSavingOf(timeMs: number, model: string, cache: number): number {\n const rates = ratesAt(timeMs, model)\n return (cache * (rates.input - rates.cacheHit)) / 1_000_000\n}\n\nconst applyRates = (rates: PricingRates, input: number, cache: number, output: number): number =>\n (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1_000_000\n\n/** Estimated CNY cost of one usage record. */\nexport function costOf(timeMs: number, model: string, input: number, cache: number, output: number): number {\n return applyRates(ratesAt(timeMs, model), input, cache, output)\n}\n\n/**\n * The same record priced under the peak/off-peak table regardless of when it\n * happened \u2014 `forceOffPeak` prices it as if it had landed in an idle window.\n * Used to answer \"what will the new pricing cost me\" before the switch and\n * \"what would shifting off-peak save\" after it.\n */\nexport function costUnderPeakEra(timeMs: number, model: string, input: number, cache: number, output: number, forceOffPeak = false): number {\n const peak = PEAK_RATES[tierOf(model)]\n const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak)\n return applyRates(rates, input, cache, output)\n}\n\n/** The price table to show the user, describing whatever is in effect now. */\nexport function pricingInfo(nowMs: number): PricingInfo {\n const split = nowMs >= PEAK_PRICING_FROM_MS\n return {\n currency: 'CNY',\n switchDate: '2026-08-17',\n splitActive: split,\n inPeakNow: split && isPeak(nowMs),\n peakWindows: ['09:00\u201312:00', '14:00\u201318:00'],\n tiers: (['pro', 'flash'] as Tier[]).map(tier => ({\n model: tier === 'pro' ? 'deepseek-v4-pro' : 'deepseek-v4-flash',\n peak: split ? PEAK_RATES[tier] : LEGACY_RATES[tier],\n offPeak: split ? halved(PEAK_RATES[tier]) : null,\n })),\n }\n}\n", "/**\n * Balance and usage data sources.\n *\n * - Balance: DeepSeek API `GET /user/balance` with the key resolved through\n * the credentials service (`DEEPSEEK_API_KEY`).\n * - Usage: token usage folded from the DSH session logs (`sessionPersistence`),\n * one record per `assistant/message` event, bucketed by local day / hour.\n */\nimport type { BalanceResponse, ModelSeriesPoint, ModelUsage, PeakSplit, PeriodUsage, SessionCost, UsageData, UsageResponse, UsageSummary } from './contract.ts'\nimport type { CredentialsFace, SessionEventFace, SessionPersistenceFace } from './context.ts'\nimport { cacheSavingOf, costOf, costUnderPeakEra, isPeak, pricingInfo } from './pricing.ts'\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))\n\nfunction errorMessage(err: unknown): string {\n return (err as { message?: string } | null)?.message ?? String(err)\n}\n\nexport async function fetchBalance(credentials: CredentialsFace | undefined): Promise<BalanceResponse> {\n if (credentials === undefined) return { ok: false, error: '\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528' }\n let cred: { value: string } | undefined\n try {\n cred = await credentials.resolve('DEEPSEEK_API_KEY')\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (cred === undefined || cred.value === '') {\n return { ok: false, error: '\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09' }\n }\n let res: Response\n try {\n res = await fetch('https://api.deepseek.com/user/balance', {\n headers: { authorization: `Bearer ${cred.value}` },\n signal: AbortSignal.timeout(20000),\n })\n } catch (err) {\n return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch {\n return { ok: false, error: '\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25' }\n }\n const p = parsed as {\n is_available?: boolean\n balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }>\n }\n return {\n ok: true,\n data: {\n isAvailable: p.is_available === true,\n balances: (p.balance_infos ?? []).map(b => ({\n currency: b.currency ?? '',\n total: b.total_balance ?? '0',\n granted: b.granted_balance ?? '0',\n toppedUp: b.topped_up_balance ?? '0',\n })),\n },\n }\n}\n\ninterface Bucket {\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nconst emptyBucket = (): Bucket => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 })\n\n/** How many sessions the ranking keeps; the rest are summarised by count. */\nconst SESSION_TOP_N = 6\n\n/**\n * The session's own title, folded from its log: `session/title` events are\n * append-only and the last one wins. It rides along with the usage replay, so\n * naming a session costs no extra read.\n */\nfunction titleOf(events: SessionEventFace[] | undefined, id: string): string {\n let title = ''\n if (events !== undefined) {\n for (const ev of events) {\n if (ev?.type !== 'session/title') continue\n const next = ev.data?.title\n if (typeof next === 'string' && next !== '') title = next\n }\n }\n return title !== '' ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`\n}\n\nconst dayKeyOf = (d: Date): string => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n\n/**\n * Roll the per-day buckets up into the windows the dashboard's summary card\n * shows. Everything is keyed by local date, matching how the buckets were\n * filled, so \"today\" means the user's today.\n */\nfunction summarize(dayMap: Map<string, Bucket>, now: Date): UsageSummary {\n const sum = (matches: (key: string) => boolean): PeriodUsage => {\n const acc: PeriodUsage = { total: 0, cost: 0, calls: 0 }\n for (const [key, bucket] of dayMap) {\n if (!matches(key)) continue\n acc.total += bucket.total\n acc.cost += bucket.cost\n acc.calls += bucket.calls\n }\n return acc\n }\n const todayKey = dayKeyOf(now)\n const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))\n const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`\n const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)\n const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`\n const dayOfMonth = now.getDate()\n return {\n today: sum(key => key === todayKey),\n yesterday: sum(key => key === yesterdayKey),\n month: sum(key => key.startsWith(monthPrefix)),\n lastMonthToDate: sum(key => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth),\n }\n}\n\n/**\n * Fold usage out of one session's event log. The model for every\n * `assistant/message` usage record is the one from the latest preceding\n * `request/header` event (each request logs one before dispatch), so usage\n * can be attributed per model. Events without any preceding header fall back\n * to the `''`/`''` (unknown) bucket.\n */\nfunction addUsageEvent(events: SessionEventFace[] | undefined, onEvent: (time: number, input: number, output: number, cache: number, reasoning: number, provider: string, model: string) => void): void {\n if (events === undefined) return\n let provider = ''\n let model = ''\n for (const ev of events) {\n if (ev?.type === 'request/header') {\n const cfg = ev.data?.header?.config\n if (cfg?.provider !== undefined && cfg?.model !== undefined) {\n provider = cfg.provider\n model = cfg.model\n }\n continue\n }\n if (ev?.type !== 'assistant/message') continue\n const usage = ev.data?.usage\n if (usage === undefined) continue\n const input = Number(usage.inputTokens) || 0\n const output = Number(usage.outputTokens) || 0\n const cache = Number(usage.cacheReadTokens) || 0\n const reasoning = Number(usage.reasoningTokens) || 0\n if (ev.time === undefined) continue\n onEvent(ev.time, input, output, cache, reasoning, provider, model)\n }\n}\n\nexport async function fetchUsage(persistence: SessionPersistenceFace | undefined): Promise<UsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n let headers: Array<{ id?: string; sessionId?: string }>\n try {\n headers = await persistence.list()\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n const dayMap = new Map<string, Bucket>()\n const hourMap = new Map<string, Bucket>()\n const overall: Bucket & { reasoning: number; cacheSavings: number } = { ...emptyBucket(), reasoning: 0, cacheSavings: 0 }\n // Per model: key `${provider}/${model}` -> totals and day/hour maps.\n const modelTotals = new Map<string, Bucket>()\n const modelDays = new Map<string, Map<string, Bucket>>()\n const modelHours = new Map<string, Map<string, Bucket>>()\n const sessions: SessionCost[] = []\n // Peak / off-peak split, plus the same usage re-priced under the new table.\n const peakSplit: PeakSplit = {\n peak: { total: 0, cost: 0, calls: 0 },\n offPeak: { total: 0, cost: 0, calls: 0 },\n peakEraCost: 0,\n offPeakEraCost: 0,\n }\n\n /** Cost is computed once per event (it depends on the model and on whether\n * the event landed in a peak window) and folded into every bucket. */\n const bump = (map: Map<string, Bucket>, key: string, input: number, output: number, cache: number, cost: number): void => {\n const bucket = map.get(key) ?? emptyBucket()\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n // reasoningTokens is a subset of outputTokens; never double-count it.\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.calls += 1\n map.set(key, bucket)\n }\n\n for (const header of headers) {\n const sid = header.id ?? header.sessionId\n if (sid === undefined || sid === '') continue\n try {\n const { events } = await persistence.readFrom(sid, 0)\n const session: SessionCost = { id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }\n addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {\n const d = new Date(time)\n const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const hourKey = String(d.getHours())\n const modelKey = `${provider}/${model}`\n const cost = costOf(time, model, input, cache, output)\n bump(dayMap, dayKey, input, output, cache, cost)\n bump(hourMap, hourKey, input, output, cache, cost)\n bump(modelTotals, modelKey, input, output, cache, cost)\n let days = modelDays.get(modelKey)\n if (days === undefined) {\n days = new Map()\n modelDays.set(modelKey, days)\n }\n bump(days, dayKey, input, output, cache, cost)\n let hours = modelHours.get(modelKey)\n if (hours === undefined) {\n hours = new Map()\n modelHours.set(modelKey, hours)\n }\n bump(hours, hourKey, input, output, cache, cost)\n overall.input += input\n overall.output += output\n overall.cache += cache\n overall.reasoning += reasoning\n overall.total += input + output + cache\n overall.cost += cost\n overall.cacheSavings += cacheSavingOf(time, model, cache)\n session.total += input + output + cache\n session.cost += cost\n session.calls += 1\n if (time > session.lastActive) session.lastActive = time\n const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak\n window.total += input + output + cache\n window.cost += cost\n window.calls += 1\n peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output)\n peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true)\n overall.calls += 1\n })\n if (session.calls > 0) {\n session.title = titleOf(events, sid)\n sessions.push(session)\n }\n } catch {\n // One broken session log must not sink the whole dashboard.\n }\n }\n\n const daily: UsageData['daily'] = []\n for (let i = 29; i >= 0; i--) {\n const d = new Date(Date.now() - i * 86_400_000)\n const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const b = dayMap.get(key) ?? emptyBucket()\n daily.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const hourly: UsageData['hourly'] = []\n for (let i = 0; i < 24; i++) {\n const b = hourMap.get(String(i)) ?? emptyBucket()\n hourly.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const heatmap: UsageData['heatmap'] = []\n for (let i = 83; i >= 0; i--) {\n const d = new Date(Date.now() - i * 86_400_000)\n const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const b = dayMap.get(key) ?? emptyBucket()\n heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const models: ModelUsage[] = []\n for (const [modelKey, bucket] of modelTotals) {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n const days = modelDays.get(modelKey)\n const hours = modelHours.get(modelKey)\n const daily: ModelSeriesPoint[] = []\n for (let i = 29; i >= 0; i--) {\n const d = new Date(Date.now() - i * 86_400_000)\n const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const b = days?.get(key) ?? emptyBucket()\n daily.push({ total: b.total, cost: b.cost, calls: b.calls })\n }\n const hourly: ModelSeriesPoint[] = []\n for (let i = 0; i < 24; i++) {\n const b = hours?.get(String(i)) ?? emptyBucket()\n hourly.push({ total: b.total, cost: b.cost, calls: b.calls })\n }\n models.push({\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n daily,\n hourly,\n })\n }\n models.sort((a, b) => b.total - a.total)\n\n return {\n ok: true,\n data: {\n daily,\n hourly,\n heatmap,\n models,\n summary: summarize(dayMap, new Date()),\n pricing: pricingInfo(Date.now()),\n peakSplit,\n sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: sessions.length,\n totals: {\n input: overall.input,\n output: overall.output,\n cache: overall.cache,\n reasoning: overall.reasoning,\n total: overall.total,\n cost: overall.cost,\n calls: overall.calls,\n cacheSavings: overall.cacheSavings,\n },\n },\n }\n}\n", "import type { ServerResponse } from 'node:http'\n\nexport function writeJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers })\n res.end(JSON.stringify(body))\n}\n", "/**\n * dsh-usage-dashboard host plugin.\n *\n * Registers the package's own fenced JSON API under\n * `/api/dsh-usage-dashboard` (balance + usage), consumed by the client half\n * through same-origin fetch. Both endpoints are memoized with a TTL so that\n * tab switches and the widget's polling don't replay session logs or hammer\n * the DeepSeek API; responses carry `Cache-Control` so the browser's HTTP\n * cache serves repeat requests without a network round trip. `?refresh=1`\n * bypasses the memo for an explicit force refresh.\n */\nimport type { HostContext } from './context.ts'\nimport { memoize } from './memo.ts'\nimport { isTrustedApiRequest } from './trust-fence.ts'\nimport { fetchBalance, fetchUsage } from './usage.ts'\nimport { writeJson } from './wire.ts'\n\n/** Plugin identity for the cordis.patch.yml row (and the client bundle id). */\nexport const name = 'dsh-usage-dashboard'\n\n/** Services required before load: the web server plus the two data sources. */\nexport const inject = ['webServer', 'credentials', 'sessionPersistence']\n\n/** Freshness windows: balance 60s (the widget also polls every 60s), usage\n * 5min (aggregating every session log is the expensive path). */\nconst BALANCE_TTL_MS = 60_000\nconst USAGE_TTL_MS = 5 * 60_000\n\n/**\n * Mount the /api/dsh-usage-dashboard routes.\n * @param ctx - host Cordis context.\n */\nexport function apply(ctx: HostContext): void {\n const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials))\n const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence))\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dsh-usage-dashboard',\n handler: async (req, res) => {\n if (!isTrustedApiRequest(req)) {\n writeJson(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n if (req.method !== 'GET') {\n writeJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const pathname = url.pathname\n const force = url.searchParams.get('refresh') === '1'\n if (pathname === '/api/dsh-usage-dashboard/balance') {\n const payload = await (force ? balanceMemo.refresh() : balanceMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(BALANCE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/usage') {\n const payload = await (force ? usageMemo.refresh() : usageMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(USAGE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else {\n writeJson(res, 404, { ok: false, error: 'not found' })\n }\n },\n }), 'dsh-usage-dashboard: /api routes')\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["daily", "hourly"]
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { BalanceResponse, UsageResponse } from '../contract.ts';
2
+ /** Last cached value (possibly stale), for an instant first render. */
3
+ export declare const getCachedBalance: () => BalanceResponse | null;
4
+ export declare const getCachedUsage: () => UsageResponse | null;
5
+ /** When the cached usage was fetched, so a consumer can say how old it is. */
6
+ export declare const getCachedUsageAt: () => number | null;
7
+ export declare const subscribeUsage: (fn: () => void) => (() => void);
8
+ export declare function fetchBalance(force?: boolean): Promise<BalanceResponse>;
9
+ export declare function fetchUsage(force?: boolean): Promise<UsageResponse>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Package-local TTL caches shared by the widget, the dashboard, and every
3
+ * tab switch. Cached values outlive the TTL: `get()` still returns them
4
+ * (marked by `at`) so a reopening tab can render instantly and refresh in
5
+ * the background, while `getFresh()` is the cache-aware fetch path.
6
+ *
7
+ * When a `storageKey` is given, every `put()` also mirrors the entry to
8
+ * localStorage so a page refresh keeps the instant first paint (the data
9
+ * shown is the last known good value; a background refresh updates it).
10
+ */
11
+ export interface CacheHit<T> {
12
+ data: T;
13
+ at: number;
14
+ }
15
+ export interface ResourceCache<T> {
16
+ /** Last cached value even when stale; null when never cached. */
17
+ get(): CacheHit<T> | null;
18
+ /** Value only when it is still within the TTL. */
19
+ getFresh(): T | null;
20
+ put(data: T): void;
21
+ }
22
+ export declare function createCache<T>(ttlMs: number, storageKey?: string): ResourceCache<T>;
@@ -0,0 +1,43 @@
1
+ /** Dependency-free bar chart and heatmap primitives. */
2
+ import { type ReactElement } from 'react';
3
+ export interface BarDatum {
4
+ label: string;
5
+ value: number;
6
+ /** Tooltip text, composed as `heading · row · row`; each ` · ` segment is
7
+ * rendered on its own line, the first as the heading. */
8
+ title?: string;
9
+ }
10
+ /** Color palette for per-model grouped bars, cycled by model order. */
11
+ export declare const MODEL_COLORS: string[];
12
+ export declare function Bars(props: {
13
+ data: BarDatum[];
14
+ height?: number;
15
+ labelEvery?: number;
16
+ }): ReactElement;
17
+ export interface HeatDatum {
18
+ date: string;
19
+ total: number;
20
+ cost: number;
21
+ calls: number;
22
+ }
23
+ /** Multi-series bar chart: one bar per series, side by side, per slot. */
24
+ export declare function GroupedBars(props: {
25
+ series: Array<{
26
+ key: string;
27
+ color: string;
28
+ bars: BarDatum[];
29
+ }>;
30
+ height?: number;
31
+ labelEvery?: number;
32
+ }): ReactElement;
33
+ /**
34
+ * Calendar heatmap with the axes it was missing: weekday labels down the left,
35
+ * month labels along the top (drawn where a column starts a new month) and a
36
+ * 少→多 scale legend, so a cell can actually be located in time.
37
+ */
38
+ export declare function Heatmap(props: {
39
+ data: HeatDatum[];
40
+ }): ReactElement;
41
+ export declare const fmt: (v: string | number) => string;
42
+ export declare const fmtInt: (v: number | undefined) => string;
43
+ export declare const fmtCompact: (v: number | undefined) => string;
@@ -0,0 +1,16 @@
1
+ /** Structural faces the client half reads from the Cordis context. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export interface SlotOptions {
4
+ name: string;
5
+ id?: string;
6
+ order?: number;
7
+ label?: string | (() => string);
8
+ }
9
+ export interface SlotsFace {
10
+ inject(key: string, callback: () => unknown): unknown;
11
+ register(options: SlotOptions, component: unknown): unknown;
12
+ }
13
+ /** The client context this plugin's apply() receives. */
14
+ export interface ClientContext extends Context {
15
+ slots: SlotsFace;
16
+ }
@@ -0,0 +1,12 @@
1
+ /** The 「额度」 conversation view tab: full balance & usage dashboard.
2
+ *
3
+ * Caching: the first render comes straight from the package-local cache
4
+ * (see ./api.ts), so reopening the tab shows data instantly. A background
5
+ * refresh then updates it without blocking; a full-screen 加载中 only
6
+ * appears when nothing has ever been cached.
7
+ */
8
+ import { type ReactElement } from 'react';
9
+ import type { ModelUsage } from '../contract.ts';
10
+ export declare const modelKeyOf: (m: ModelUsage) => string;
11
+ export declare const modelNameOf: (m: ModelUsage) => string;
12
+ export declare function BalanceDashboard(): ReactElement;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * dsh-usage-dashboard client half: mounts the floating balance widget
3
+ * (`shell.overlay`) and the 「额度」 view tab (`conversation.view`).
4
+ * Data arrives through the package's own host API via same-origin fetch.
5
+ */
6
+ import type { ClientContext } from './context.ts';
7
+ /** Plugin identity for the client bundle. */
8
+ export declare const name = "dsh-usage-dashboard";
9
+ /** Services required before load. */
10
+ export declare const inject: string[];
11
+ /**
12
+ * Mount the two surfaces.
13
+ * @param ctx - client Cordis context.
14
+ */
15
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * localStorage-backed user preferences for the plugin.
3
+ *
4
+ * Everything the user deliberately sets (widget visibility, the corner it was
5
+ * dragged to, whether it is collapsed) belongs here so a page refresh does not
6
+ * throw the choice away. Keys share one prefix and carry a version suffix so
7
+ * the shape can evolve without reading back stale values.
8
+ *
9
+ * Every read is validated by the caller's guard: a corrupted or hand-edited
10
+ * entry falls back instead of poisoning the UI.
11
+ */
12
+ export declare function loadPref<T>(key: string, isValid: (value: unknown) => boolean, fallback: T): T;
13
+ export declare function savePref(key: string, value: unknown): void;
14
+ export declare const isBoolean: (value: unknown) => boolean;