dsh-balance-widget 0.4.1 → 0.4.3
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/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +151 -11
- package/lib/index.js +122 -14
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -164,7 +164,7 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
164
164
|
|
|
165
165
|
- Config tree: `dsh --profile web --dump-config` should show a `balance-widget` entry.
|
|
166
166
|
- Balance route: after restarting dsh web, `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` should return `{ ok, balance_infos, modelId }`.
|
|
167
|
-
- Cost routes: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session
|
|
167
|
+
- Cost routes: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` and `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ cost, inputTokens, outputTokens, modelId }`.
|
|
168
168
|
|
|
169
169
|
## License
|
|
170
170
|
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
162
162
|
|
|
163
163
|
- 配置树:`dsh --profile web --dump-config` 应出现 `balance-widget` 条目
|
|
164
164
|
- 余额路由:重启 dsh web 后 `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` 应返回 `{ ok, balance_infos, modelId }`
|
|
165
|
-
- 成本路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session
|
|
165
|
+
- 成本路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 与 `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ cost, inputTokens, outputTokens, modelId }`
|
|
166
166
|
|
|
167
167
|
## License
|
|
168
168
|
|
package/lib/client.js
CHANGED
|
@@ -132,9 +132,24 @@ window.__ModuleLoader__.load({
|
|
|
132
132
|
/** Same-origin cost routes. */
|
|
133
133
|
const LAST_COST_ROUTE = "/api/dsh-balance/last-cost";
|
|
134
134
|
const TODAY_COST_ROUTE = "/api/dsh-balance/today-cost";
|
|
135
|
+
const ACTIVE_COST_ROUTE = "/api/dsh-balance/active-cost";
|
|
135
136
|
/** Official DeepSeek platform top-up page (opens in a new tab). */
|
|
136
137
|
const TOP_UP_URL = "https://platform.deepseek.com/top_up";
|
|
137
138
|
/**
|
|
139
|
+
* Fetch the active session's costs (last prompt + session total).
|
|
140
|
+
* @returns { ok, lastPrompt?, sessionTotal?, error? }
|
|
141
|
+
*/
|
|
142
|
+
async function fetchActiveCost() {
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(ACTIVE_COST_ROUTE, { headers: { accept: "application/json" } });
|
|
145
|
+
const payload = await response.json().catch(() => ({}));
|
|
146
|
+
if (!response.ok) return { ok: false, error: payload.error ?? `active-cost responded ${response.status}` };
|
|
147
|
+
return { ok: true, lastPrompt: payload.lastPrompt?.cost ?? 0, sessionTotal: payload.sessionTotal?.cost ?? 0 };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
138
153
|
* Fetch the account balance from the host proxy.
|
|
139
154
|
* @returns { ok, balanceInfos?, modelId?, error? }
|
|
140
155
|
*/
|
|
@@ -193,6 +208,28 @@ window.__ModuleLoader__.load({
|
|
|
193
208
|
return { symbol, total, currency: entry.currency };
|
|
194
209
|
}
|
|
195
210
|
//#endregion
|
|
211
|
+
//#region lib/types/client/ExplainIcon.js
|
|
212
|
+
/**
|
|
213
|
+
* A small ⓘ marker with a hover tooltip explaining a term. Wraps the
|
|
214
|
+
* official DSH Tooltip so the popover stays visually consistent.
|
|
215
|
+
* @param {Object} props - label (tooltip text), t (locale).
|
|
216
|
+
*/
|
|
217
|
+
function ExplainIcon({ label, t }) {
|
|
218
|
+
return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
219
|
+
label: label,
|
|
220
|
+
side: "top",
|
|
221
|
+
delayMs: 300,
|
|
222
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
223
|
+
role: "img",
|
|
224
|
+
"aria-label": t("explain.aria"),
|
|
225
|
+
style: { cursor: "help", color: "var(--dsw-alias-label-tertiary)", marginLeft: 3, fontSize: 11, lineHeight: 1 },
|
|
226
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
227
|
+
children: "ⓘ"
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
196
233
|
//#region lib/types/client/SidebarBalanceCard.js
|
|
197
234
|
/**
|
|
198
235
|
* Sidebar footer card: balance + remaining-ratio bar + today cost.
|
|
@@ -204,17 +241,32 @@ window.__ModuleLoader__.load({
|
|
|
204
241
|
function SidebarBalanceCard({ wide, t }) {
|
|
205
242
|
const [open, setOpen] = (0, react.useState)(false);
|
|
206
243
|
const [balance, setBalance] = (0, react.useState)(null);
|
|
207
|
-
const [
|
|
244
|
+
const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
|
|
245
|
+
const [sessionTotal, setSessionTotal] = (0, react.useState)(null);
|
|
246
|
+
const [todayWs, setTodayWs] = (0, react.useState)(null);
|
|
247
|
+
const [todayAll, setTodayAll] = (0, react.useState)(null);
|
|
208
248
|
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
209
249
|
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
210
250
|
const [error, setError] = (0, react.useState)(null);
|
|
211
251
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
252
|
+
/** Fetch today's costs (workspace + all) from the dual-value route. */
|
|
253
|
+
const fetchTodayCosts = async () => {
|
|
254
|
+
const response = await fetch(TODAY_COST_ROUTE, { headers: { accept: "application/json" } });
|
|
255
|
+
const payload = await response.json().catch(() => ({}));
|
|
256
|
+
if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
workspace: payload.workspace?.cost ?? 0,
|
|
260
|
+
all: payload.all?.cost ?? 0
|
|
261
|
+
};
|
|
262
|
+
};
|
|
212
263
|
const refresh = async (showLoading) => {
|
|
213
264
|
if (showLoading) setLoading(true);
|
|
214
265
|
setError(null);
|
|
215
|
-
const [bResult, tResult] = await Promise.all([
|
|
266
|
+
const [bResult, aResult, tResult] = await Promise.all([
|
|
216
267
|
fetchBalance(),
|
|
217
|
-
|
|
268
|
+
fetchActiveCost(),
|
|
269
|
+
fetchTodayCosts()
|
|
218
270
|
]);
|
|
219
271
|
const failures = [];
|
|
220
272
|
if (bResult.ok) {
|
|
@@ -224,8 +276,18 @@ window.__ModuleLoader__.load({
|
|
|
224
276
|
} else {
|
|
225
277
|
failures.push(`余额: ${bResult.error}`);
|
|
226
278
|
}
|
|
227
|
-
if (
|
|
228
|
-
|
|
279
|
+
if (aResult.ok) {
|
|
280
|
+
setLastPrompt(aResult.lastPrompt);
|
|
281
|
+
setSessionTotal(aResult.sessionTotal);
|
|
282
|
+
} else {
|
|
283
|
+
failures.push(`会话: ${aResult.error}`);
|
|
284
|
+
}
|
|
285
|
+
if (tResult.ok) {
|
|
286
|
+
setTodayWs(tResult.workspace);
|
|
287
|
+
setTodayAll(tResult.all);
|
|
288
|
+
} else {
|
|
289
|
+
failures.push(`今日: ${tResult.error}`);
|
|
290
|
+
}
|
|
229
291
|
setError(failures.length > 0 ? failures.join(";") : null);
|
|
230
292
|
if (showLoading) setLoading(false);
|
|
231
293
|
};
|
|
@@ -291,7 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
291
353
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
292
354
|
children: t("todayCostShort")
|
|
293
355
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
294
|
-
children:
|
|
356
|
+
children: todayAll !== null ? formatCny(todayAll) : "—"
|
|
295
357
|
})]
|
|
296
358
|
})
|
|
297
359
|
})]
|
|
@@ -318,7 +380,13 @@ window.__ModuleLoader__.load({
|
|
|
318
380
|
className: styles.row,
|
|
319
381
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
320
382
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
321
|
-
|
|
383
|
+
className: styles.label,
|
|
384
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
385
|
+
children: [t("balance"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
386
|
+
label: t("explain.balance"),
|
|
387
|
+
t
|
|
388
|
+
})]
|
|
389
|
+
})
|
|
322
390
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
323
391
|
className: styles.val,
|
|
324
392
|
"data-level": level > 0 ? level : undefined,
|
|
@@ -333,10 +401,64 @@ window.__ModuleLoader__.load({
|
|
|
333
401
|
className: styles.row,
|
|
334
402
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
335
403
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
336
|
-
|
|
404
|
+
className: styles.label,
|
|
405
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
406
|
+
children: [t("lastPrompt"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
407
|
+
label: t("explain.lastPrompt"),
|
|
408
|
+
t
|
|
409
|
+
})]
|
|
410
|
+
})
|
|
411
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
412
|
+
className: styles.val,
|
|
413
|
+
children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
|
|
414
|
+
})]
|
|
415
|
+
})
|
|
416
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
417
|
+
className: styles.row,
|
|
418
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
419
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
420
|
+
className: styles.label,
|
|
421
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
422
|
+
children: [t("sessionTotal"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
423
|
+
label: t("explain.sessionTotal"),
|
|
424
|
+
t
|
|
425
|
+
})]
|
|
426
|
+
})
|
|
427
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
428
|
+
className: styles.val,
|
|
429
|
+
children: loading ? "…" : sessionTotal !== null ? formatCny(sessionTotal) : "—"
|
|
430
|
+
})]
|
|
431
|
+
})
|
|
432
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
433
|
+
className: styles.row,
|
|
434
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
435
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
436
|
+
className: styles.label,
|
|
437
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
438
|
+
children: [t("todayWs"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
439
|
+
label: t("explain.todayWs"),
|
|
440
|
+
t
|
|
441
|
+
})]
|
|
442
|
+
})
|
|
443
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
444
|
+
className: styles.val,
|
|
445
|
+
children: loading ? "…" : todayWs !== null ? formatCny(todayWs) : "—"
|
|
446
|
+
})]
|
|
447
|
+
})
|
|
448
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
449
|
+
className: styles.row,
|
|
450
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
451
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
452
|
+
className: styles.label,
|
|
453
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
454
|
+
children: [t("todayAll"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
455
|
+
label: t("explain.todayAll"),
|
|
456
|
+
t
|
|
457
|
+
})]
|
|
458
|
+
})
|
|
337
459
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
338
460
|
className: styles.val,
|
|
339
|
-
children: loading ? "…" :
|
|
461
|
+
children: loading ? "…" : todayAll !== null ? formatCny(todayAll) : "—"
|
|
340
462
|
})]
|
|
341
463
|
})
|
|
342
464
|
}), error !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -378,7 +500,16 @@ window.__ModuleLoader__.load({
|
|
|
378
500
|
"refresh.aria": "刷新数据",
|
|
379
501
|
"refresh.title": "刷新余额与成本",
|
|
380
502
|
"balance": "账户余额",
|
|
381
|
-
"
|
|
503
|
+
"lastPrompt": "最近一次提问",
|
|
504
|
+
"sessionTotal": "本会话成本",
|
|
505
|
+
"todayWs": "今日·本项目",
|
|
506
|
+
"todayAll": "今日·全部",
|
|
507
|
+
"explain.aria": "解释",
|
|
508
|
+
"explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
|
|
509
|
+
"explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
|
|
510
|
+
"explain.sessionTotal": "最近活跃会话从开始到现在的累计费用估算。",
|
|
511
|
+
"explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
|
|
512
|
+
"explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
|
|
382
513
|
"warn.title.1": "余额不足 ¥5,建议及时充值",
|
|
383
514
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
384
515
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
@@ -396,7 +527,16 @@ window.__ModuleLoader__.load({
|
|
|
396
527
|
"refresh.aria": "Refresh data",
|
|
397
528
|
"refresh.title": "Refresh balance and costs",
|
|
398
529
|
"balance": "Account balance",
|
|
399
|
-
"
|
|
530
|
+
"lastPrompt": "Last prompt",
|
|
531
|
+
"sessionTotal": "This session",
|
|
532
|
+
"todayWs": "Today · this project",
|
|
533
|
+
"todayAll": "Today · all",
|
|
534
|
+
"explain.aria": "Explain",
|
|
535
|
+
"explain.balance": "DeepSeek account balance, fetched live from the official API.",
|
|
536
|
+
"explain.lastPrompt": "Estimated cost of the latest question (incl. the AI reply) in the most recent session.",
|
|
537
|
+
"explain.sessionTotal": "Estimated total cost of the most recent session since it started.",
|
|
538
|
+
"explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
|
|
539
|
+
"explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
|
|
400
540
|
"warn.title.1": "Balance below ¥5, consider topping up",
|
|
401
541
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
402
542
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,8 @@ export const API = {
|
|
|
24
24
|
balance: "/api/dsh-balance/balance",
|
|
25
25
|
cost: "/api/dsh-balance/cost",
|
|
26
26
|
lastCost: "/api/dsh-balance/last-cost",
|
|
27
|
-
todayCost: "/api/dsh-balance/today-cost"
|
|
27
|
+
todayCost: "/api/dsh-balance/today-cost",
|
|
28
|
+
activeCost: "/api/dsh-balance/active-cost"
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/** Default DeepSeek API base (mirrors dsh-llm-deepseek's PUBLIC_BASE_URL). */
|
|
@@ -459,6 +460,7 @@ async function decompressSession(path) {
|
|
|
459
460
|
function parseSessionUsages(text) {
|
|
460
461
|
const samples = [];
|
|
461
462
|
let currentModel = void 0;
|
|
463
|
+
let cwd = void 0;
|
|
462
464
|
for (const line of text.split("\n")) {
|
|
463
465
|
if (line === "") continue;
|
|
464
466
|
let event;
|
|
@@ -468,7 +470,9 @@ function parseSessionUsages(text) {
|
|
|
468
470
|
continue;
|
|
469
471
|
}
|
|
470
472
|
const type = event.type;
|
|
471
|
-
if (type === "
|
|
473
|
+
if (type === "session") {
|
|
474
|
+
cwd = event.cwd;
|
|
475
|
+
} else if (type === "request/header") {
|
|
472
476
|
currentModel = event.data?.header?.config?.model ?? currentModel;
|
|
473
477
|
} else if (type === "assistant/message") {
|
|
474
478
|
const usage = event.data?.usage;
|
|
@@ -482,7 +486,7 @@ function parseSessionUsages(text) {
|
|
|
482
486
|
}
|
|
483
487
|
}
|
|
484
488
|
}
|
|
485
|
-
return samples;
|
|
489
|
+
return { samples, cwd };
|
|
486
490
|
}
|
|
487
491
|
|
|
488
492
|
/** Cost of the last turn (the most recent user prompt) in one session file. */
|
|
@@ -502,19 +506,71 @@ function lastTurnCost(samples) {
|
|
|
502
506
|
return { cost, inputTokens: input, outputTokens: output };
|
|
503
507
|
}
|
|
504
508
|
|
|
509
|
+
/** Total cost across every sample of one session (all turns). */
|
|
510
|
+
function sessionTotalCost(samples) {
|
|
511
|
+
let cost = 0;
|
|
512
|
+
let input = 0;
|
|
513
|
+
let output = 0;
|
|
514
|
+
for (const s of samples) {
|
|
515
|
+
cost += priceUsage(s.usage, s.model, s.time);
|
|
516
|
+
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
517
|
+
output += s.usage.outputTokens ?? 0;
|
|
518
|
+
}
|
|
519
|
+
return { cost, inputTokens: input, outputTokens: output };
|
|
520
|
+
}
|
|
521
|
+
|
|
505
522
|
/**
|
|
506
|
-
*
|
|
523
|
+
* Find the session file modified most recently (the "active" session).
|
|
524
|
+
* Walks the sessions tree and picks by mtime; returns the path or undefined.
|
|
525
|
+
*/
|
|
526
|
+
async function findMostRecentSessionFile() {
|
|
527
|
+
const root = sessionsRoot();
|
|
528
|
+
try {
|
|
529
|
+
const fs = await awaitImportFs();
|
|
530
|
+
const { statSync } = fs;
|
|
531
|
+
let best = void 0;
|
|
532
|
+
let bestMtime = -1;
|
|
533
|
+
const walk = (dir) => {
|
|
534
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
535
|
+
const full = `${dir}/${entry.name}`;
|
|
536
|
+
if (entry.isDirectory()) {
|
|
537
|
+
walk(full);
|
|
538
|
+
} else if (entry.name === "session.jsonl.zstd") {
|
|
539
|
+
try {
|
|
540
|
+
const mtime = statSync(full).mtimeMs;
|
|
541
|
+
if (mtime > bestMtime) {
|
|
542
|
+
bestMtime = mtime;
|
|
543
|
+
best = full;
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
/* ignore unreadable files */
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
walk(root);
|
|
552
|
+
return best;
|
|
553
|
+
} catch {
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Sum today's usage across session files. "Today" = events whose time
|
|
507
560
|
* falls on the same calendar day (local time) as now.
|
|
561
|
+
* @param {Array<{samples:Array,cwd?:string}>} sessionList - parsed sessions.
|
|
562
|
+
* @param {string} [cwdFilter] - if given, only sessions with this cwd count.
|
|
508
563
|
*/
|
|
509
|
-
function todayCost(
|
|
564
|
+
function todayCost(sessionList, cwdFilter) {
|
|
510
565
|
const now = new Date();
|
|
511
566
|
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
512
567
|
const dayEnd = dayStart + 24 * 60 * 60 * 1000;
|
|
513
568
|
let cost = 0;
|
|
514
569
|
let input = 0;
|
|
515
570
|
let output = 0;
|
|
516
|
-
for (const
|
|
517
|
-
|
|
571
|
+
for (const session of sessionList) {
|
|
572
|
+
if (cwdFilter !== void 0 && session.cwd !== cwdFilter) continue;
|
|
573
|
+
for (const s of session.samples) {
|
|
518
574
|
if (s.time < dayStart || s.time >= dayEnd) continue;
|
|
519
575
|
cost += priceUsage(s.usage, s.model, s.time);
|
|
520
576
|
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
@@ -545,7 +601,8 @@ async function collectAllSessionSamples() {
|
|
|
545
601
|
for (const file of files) {
|
|
546
602
|
const result = await decompressSession(file);
|
|
547
603
|
if (!result.ok) continue;
|
|
548
|
-
|
|
604
|
+
const parsed = parseSessionUsages(result.text);
|
|
605
|
+
perSession.push(parsed);
|
|
549
606
|
}
|
|
550
607
|
return perSession;
|
|
551
608
|
}
|
|
@@ -622,8 +679,8 @@ function makeRoutes(ctx, config) {
|
|
|
622
679
|
const compute = async () => {
|
|
623
680
|
const result = await decompressSession(file);
|
|
624
681
|
if (!result.ok) return { error: result.error };
|
|
625
|
-
const
|
|
626
|
-
return { value: lastTurnCost(samples) };
|
|
682
|
+
const parsed = parseSessionUsages(result.text);
|
|
683
|
+
return { value: lastTurnCost(parsed.samples) };
|
|
627
684
|
};
|
|
628
685
|
const outcome = await dedupe(key, () => cachedOrCompute(key, compute));
|
|
629
686
|
if (outcome.error !== void 0) {
|
|
@@ -640,12 +697,63 @@ function makeRoutes(ctx, config) {
|
|
|
640
697
|
if (!guard(req, res, "GET")) return;
|
|
641
698
|
const key = "today";
|
|
642
699
|
const compute = async () => {
|
|
643
|
-
const
|
|
644
|
-
|
|
700
|
+
const sessionList = await collectAllSessionSamples();
|
|
701
|
+
const all = todayCost(sessionList);
|
|
702
|
+
// "Current workspace": the cwd of the session with the most
|
|
703
|
+
// samples today (best proxy for where the user is working).
|
|
704
|
+
let activeCwd = void 0;
|
|
705
|
+
let activeCount = 0;
|
|
706
|
+
for (const session of sessionList) {
|
|
707
|
+
if (session.cwd === void 0) continue;
|
|
708
|
+
const todaySamples = session.samples.filter((s) => {
|
|
709
|
+
const now = new Date();
|
|
710
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
711
|
+
return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
|
|
712
|
+
}).length;
|
|
713
|
+
if (todaySamples > activeCount) {
|
|
714
|
+
activeCount = todaySamples;
|
|
715
|
+
activeCwd = session.cwd;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const workspace = todayCost(sessionList, activeCwd);
|
|
719
|
+
return {
|
|
720
|
+
value: {
|
|
721
|
+
workspace: { ...workspace, cwd: activeCwd },
|
|
722
|
+
all
|
|
723
|
+
}
|
|
724
|
+
};
|
|
645
725
|
};
|
|
646
726
|
const outcome = await dedupe(key, () => cachedOrCompute(key, compute));
|
|
647
727
|
writeJson(res, 200, { ...outcome.value, modelId: config.modelId, pricingSource, pricingSyncedAt });
|
|
648
728
|
}
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
kind: "exact",
|
|
732
|
+
path: API.activeCost,
|
|
733
|
+
handler: async (req, res) => {
|
|
734
|
+
if (!guard(req, res, "GET")) return;
|
|
735
|
+
const file = await findMostRecentSessionFile();
|
|
736
|
+
if (file === void 0) {
|
|
737
|
+
writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const result = await decompressSession(file);
|
|
741
|
+
if (!result.ok) {
|
|
742
|
+
writeJson(res, 502, { error: result.error, modelId: config.modelId });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const parsed = parseSessionUsages(result.text);
|
|
746
|
+
const last = lastTurnCost(parsed.samples);
|
|
747
|
+
const total = sessionTotalCost(parsed.samples);
|
|
748
|
+
writeJson(res, 200, {
|
|
749
|
+
lastPrompt: last,
|
|
750
|
+
sessionTotal: total,
|
|
751
|
+
cwd: parsed.cwd,
|
|
752
|
+
modelId: config.modelId,
|
|
753
|
+
pricingSource,
|
|
754
|
+
pricingSyncedAt
|
|
755
|
+
});
|
|
756
|
+
}
|
|
649
757
|
}
|
|
650
758
|
];
|
|
651
759
|
}
|
|
@@ -689,8 +797,8 @@ function buildBillingTool(ctx, config) {
|
|
|
689
797
|
}
|
|
690
798
|
if (query === "cost" || query === "both") {
|
|
691
799
|
try {
|
|
692
|
-
const
|
|
693
|
-
const today = todayCost(
|
|
800
|
+
const sessionList = await collectAllSessionSamples();
|
|
801
|
+
const today = todayCost(sessionList);
|
|
694
802
|
parts.push(`Today's estimated cost: CNY ${today.cost.toFixed(4)} (${today.inputTokens} input tokens, ${today.outputTokens} output tokens)`);
|
|
695
803
|
} catch (error) {
|
|
696
804
|
parts.push(`Cost query failed: ${error instanceof Error ? error.message : String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-balance-widget",
|
|
3
3
|
"description": "Balance & cost widget for the dsh web GUI: a corner icon in the conversation session header that shows the DeepSeek account balance and the current session's estimated spend on click. Host half proxies the official /user/balance API over ctx.webServer (loopback-only); client half renders the icon and popover. Zero external dependencies — works on Node 24.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|