@lyk308/dsh-token-dashboard 0.1.0 → 0.1.1
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.md +60 -55
- package/README.zh.md +60 -55
- package/lib/client.js +52 -4
- package/lib/index.js +64 -8
- package/package.json +41 -49
package/README.md
CHANGED
|
@@ -1,55 +1,60 @@
|
|
|
1
|
-
# dsh-token-dashboard
|
|
2
|
-
|
|
3
|
-
[](https://awesome-dsh-plugin.com)
|
|
4
|
-
|
|
5
|
-
A DeepSeek Harness plugin that puts your **real-time account balance** and **token usage** on one dashboard. Local-first: reads your DSH session logs and the DeepSeek `/user/balance` API. No cloud account beyond your existing DeepSeek key.
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
# dsh-token-dashboard
|
|
2
|
+
|
|
3
|
+
[](https://awesome-dsh-plugin.com)
|
|
4
|
+
|
|
5
|
+
A DeepSeek Harness plugin that puts your **real-time account balance** and **token usage** on one dashboard. Local-first: reads your DSH session logs and the DeepSeek `/user/balance` API. No cloud account beyond your existing DeepSeek key.
|
|
6
|
+
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 💰 **Account balance** — live balance from the DeepSeek official `/user/balance` API (CNY/USD aware), with force-refresh.
|
|
16
|
+
- 📊 **Token usage** — Today / This month / All-time cards, plus request count, cache hit rate, and session count.
|
|
17
|
+
- 📈 **Usage distribution** — toggle between **by model** and **by workspace**, with a proportional share bar (top 8 + "other" aggregate). Percentages match the bar exactly.
|
|
18
|
+
- 🗓 **Activity heatmap** — 30 / 90 / 180 day switch, green-shade intensity maps token usage.
|
|
19
|
+
- 🤖 **Model breakdown** — a table of Req / Input / Cache / Output per model.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
This is a standard DSH bundle plugin, so install it with the official CLI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# From GitHub
|
|
27
|
+
dsh plugin --profile web add github:lyk308/dsh-token-dashboard
|
|
28
|
+
|
|
29
|
+
# From npm
|
|
30
|
+
dsh plugin --profile web add @lyk308/dsh-token-dashboard
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
After installing, **refresh the web UI**. The dashboard appears under **Settings → 用量看板** (Usage Dashboard).
|
|
34
|
+
|
|
35
|
+
> Note: the plugin reads the DeepSeek API key from DSH's credentials service (`DEEPSEEK_API_KEY`). Without a valid key the balance card shows a hint instead of a number; token usage still works because it comes from local session logs.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
1. Open **Settings → 用量看板**.
|
|
40
|
+
2. Refresh the server to load the latest data at any time.
|
|
41
|
+
3. Switch between **by model / by workspace** and the **30 / 90 / 180-day** heatmap ranges.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
The plugin mostly works with no configuration. The host half resolves sensible defaults in `lib/index.js`:
|
|
46
|
+
|
|
47
|
+
- `sessionsRoot` — defaults to `$DSH_HOME/sessions`.
|
|
48
|
+
- `balanceCacheMs` — balance cache TTL (default 300000 ms = 5 min).
|
|
49
|
+
- `sweepIntervalMs` — stats refresh interval (default 60000 ms).
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
- `lib/index.js` — host half: session scanning (multi-frame zstd), token aggregation, balance proxy.
|
|
54
|
+
- `lib/client.js` — client half: the settings.section UI.
|
|
55
|
+
|
|
56
|
+
Edit `lib/client.js` → refresh the page to see changes. Edit `lib/index.js` → restart the DSH web profile.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
[MIT](./LICENSE)
|
package/README.zh.md
CHANGED
|
@@ -1,55 +1,60 @@
|
|
|
1
|
-
# dsh-token-dashboard
|
|
2
|
-
|
|
3
|
-
[](https://awesome-dsh-plugin.com)
|
|
4
|
-
|
|
5
|
-
一个 DeepSeek Harness 插件,把**实时账户余额**和 **Token 使用量**整合在一个看板上。本地优先:读取你的 DSH 会话日志 + DeepSeek 官方 `/user/balance` 接口,无需额外云端账号(沿用你已有的 DeepSeek key)。
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
# dsh-token-dashboard
|
|
2
|
+
|
|
3
|
+
[](https://awesome-dsh-plugin.com)
|
|
4
|
+
|
|
5
|
+
一个 DeepSeek Harness 插件,把**实时账户余额**和 **Token 使用量**整合在一个看板上。本地优先:读取你的 DSH 会话日志 + DeepSeek 官方 `/user/balance` 接口,无需额外云端账号(沿用你已有的 DeepSeek key)。
|
|
6
|
+
|
|
7
|
+
## 效果预览
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## 功能
|
|
14
|
+
|
|
15
|
+
- 💰 **账户余额** — 通过 DeepSeek 官方 `/user/balance` 接口实时获取(自动识别 CNY/USD),支持强制刷新。
|
|
16
|
+
- 📊 **Token 使用量** — 今日 / 本月 / 总计三卡,加请求数、缓存命中率、会话数。
|
|
17
|
+
- 📈 **用量分布** — 在**按模型**和**按工作区**之间切换,带比例占比条(前 8 项 + 「其他」聚合),百分比与占比条完全一致。
|
|
18
|
+
- 🗓 **活跃度热力图** — 30 / 90 / 180 天切换,绿色深浅映射 Token 用量。
|
|
19
|
+
- 🤖 **按模型明细** — 每个模型的 Req / Input / Cache / Output 表格。
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
这是一个标准 DSH bundle 插件,用官方 CLI 安装:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 从 GitHub 安装
|
|
27
|
+
dsh plugin --profile web add github:lyk308/dsh-token-dashboard
|
|
28
|
+
|
|
29
|
+
# 从 npm 安装
|
|
30
|
+
dsh plugin --profile web add @lyk308/dsh-token-dashboard
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
安装后**刷新网页**即可。看板位于 **设置 → 用量看板**。
|
|
34
|
+
|
|
35
|
+
> 说明:插件从 DSH 的凭据服务读取 DeepSeek API Key(`DEEPSEEK_API_KEY`)。没有有效 key 时余额卡片显示引导文案;Token 用量仍可正常统计(来自本地会话日志)。
|
|
36
|
+
|
|
37
|
+
## 使用方法
|
|
38
|
+
|
|
39
|
+
1. 打开 **设置 → 用量看板**。
|
|
40
|
+
2. 随时刷新服务器加载最新数据。
|
|
41
|
+
3. 切换 **按模型 / 按工作区**,以及 **30 / 90 / 180 天**热力图范围。
|
|
42
|
+
|
|
43
|
+
## 配置
|
|
44
|
+
|
|
45
|
+
插件基本无需配置即可使用,host 半在 `lib/index.js` 里解析合理默认值:
|
|
46
|
+
|
|
47
|
+
- `sessionsRoot` — 默认为 `$DSH_HOME/sessions`。
|
|
48
|
+
- `balanceCacheMs` — 余额缓存时长(默认 300000 毫秒 = 5 分钟)。
|
|
49
|
+
- `sweepIntervalMs` — 统计刷新间隔(默认 60000 毫秒)。
|
|
50
|
+
|
|
51
|
+
## 开发
|
|
52
|
+
|
|
53
|
+
- `lib/index.js` — host 半:会话扫描(多帧 zstd)、Token 聚合、余额代理。
|
|
54
|
+
- `lib/client.js` — client 半:settings.section 界面。
|
|
55
|
+
|
|
56
|
+
改 `lib/client.js` → 刷新页面即可看到变化。改 `lib/index.js` → 重启 DSH web profile。
|
|
57
|
+
|
|
58
|
+
## 许可证
|
|
59
|
+
|
|
60
|
+
[MIT](./LICENSE)
|
package/lib/client.js
CHANGED
|
@@ -94,6 +94,7 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
const [balanceLoading, setBalanceLoading] = useState(true);
|
|
95
95
|
const [groupBy, setGroupBy] = useState("model"); // "model" | "workspace"
|
|
96
96
|
const [range, setRange] = useState(90); // 30 | 90 | 180 days
|
|
97
|
+
const [metric, setMetric] = useState("cost"); // "cost" | "token"
|
|
97
98
|
|
|
98
99
|
const loadStats = useCallback(async () => {
|
|
99
100
|
try {
|
|
@@ -162,14 +163,18 @@ window.__ModuleLoader__.load({
|
|
|
162
163
|
// both reflect "share of all usage".
|
|
163
164
|
const groupSum = groupEntries.reduce((s, g) => s + (Number(g.total) || 0), 0);
|
|
164
165
|
|
|
165
|
-
// heatmap: build days from byDay within range
|
|
166
|
+
// heatmap: build days from byDay within range. byDay entries are either
|
|
167
|
+
// {total, cost, ...} objects (new) or a bare number (legacy), so normalize.
|
|
166
168
|
const byDay = stats.byDay || {};
|
|
167
169
|
const todayD = new Date();
|
|
168
170
|
const cells = [];
|
|
169
171
|
for (let i = range - 1; i >= 0; i--) {
|
|
170
172
|
const d = new Date(todayD); d.setDate(d.getDate() - i);
|
|
171
173
|
const key = d.toISOString().slice(0, 10);
|
|
172
|
-
|
|
174
|
+
const rec = byDay[key];
|
|
175
|
+
const total = (rec && typeof rec === "object") ? Number(rec.total) || 0 : Number(rec) || 0;
|
|
176
|
+
const cost = (rec && typeof rec === "object") ? Number(rec.cost) || 0 : 0;
|
|
177
|
+
cells.push({ key, val: total, cost });
|
|
173
178
|
}
|
|
174
179
|
const heatMax = Math.max(1, ...cells.map((c) => c.val));
|
|
175
180
|
// group cells into weeks for the grid (7 per week)
|
|
@@ -234,18 +239,43 @@ window.__ModuleLoader__.load({
|
|
|
234
239
|
jsx("div", { style: { width: "14px", height: "14px", borderRadius: "2px", background: heatColor(c.val, heatMax) }, title: c.key + ": " + fmt(c.val) }, c.key)
|
|
235
240
|
) });
|
|
236
241
|
|
|
242
|
+
// --- trend chart: a lightweight SVG polyline of daily metric over `range`.
|
|
243
|
+
// metric = "cost" or "token". Uses only inline SVG — no chart lib.
|
|
244
|
+
const trendValues = cells.map((c) => (metric === "cost" ? c.cost : c.val));
|
|
245
|
+
const tMax = Math.max(1, ...trendValues);
|
|
246
|
+
const TW = 320, TH = 80, PAD = 6;
|
|
247
|
+
let trendSvg;
|
|
248
|
+
if (trendValues.length <= 1) {
|
|
249
|
+
trendSvg = jsx("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "8px 0" }, children: "数据不足" });
|
|
250
|
+
} else {
|
|
251
|
+
const step = TW / (trendValues.length - 1);
|
|
252
|
+
const pts = trendValues.map((v, i) => {
|
|
253
|
+
const x = PAD + i * step;
|
|
254
|
+
const y = TH - PAD - (v / tMax) * (TH - 2 * PAD);
|
|
255
|
+
return x.toFixed(1) + "," + y.toFixed(1);
|
|
256
|
+
}).join(" ");
|
|
257
|
+
const lastPt = pts.split(" ")[pts.split(" ").length - 1];
|
|
258
|
+
const lastV = trendValues[trendValues.length - 1];
|
|
259
|
+
trendSvg = jsxs("svg", { width: "100%", viewBox: "0 0 " + TW + " " + TH, style: { display: "block", maxHeight: "90px", width: "100%" }, children: [
|
|
260
|
+
jsx("polyline", { points: pts, fill: "none", stroke: "var(--dsw-alias-label-primary)", strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round" }),
|
|
261
|
+
jsx("circle", { cx: lastPt.split(",")[0], cy: lastPt.split(",")[1], r: "3", fill: "var(--dsw-alias-label-primary)" }),
|
|
262
|
+
jsx("text", { x: "10", y: "12", fill: "var(--dsw-alias-label-tertiary)", fontSize: "10", children: (metric === "cost" ? ("¥" + fmtMoney(lastV)) : fmt(lastV)) })
|
|
263
|
+
] });
|
|
264
|
+
}
|
|
265
|
+
|
|
237
266
|
const modelTable = jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: "13px" }, children: [
|
|
238
267
|
jsxs("thead", { children: [jsxs("tr", { children: [
|
|
239
268
|
jsx("th", { style: thStyle, children: "模型" }), jsx("th", { style: thR, children: "Req" }),
|
|
240
269
|
jsx("th", { style: thR, children: "Input" }), jsx("th", { style: thR, children: "Cache" }),
|
|
241
|
-
jsx("th", { style: thR, children: "Output" })
|
|
270
|
+
jsx("th", { style: thR, children: "Output" }), jsx("th", { style: thR, children: "Cost" })
|
|
242
271
|
] })] }),
|
|
243
272
|
jsxs("tbody", { children: modelRows.map((r) => jsxs("tr", { style: { borderTop: "1px solid var(--dsw-alias-border-l1)" }, children: [
|
|
244
273
|
jsx("td", { style: tdStyle, children: r.key }),
|
|
245
274
|
jsx("td", { style: tdR, children: r.req }),
|
|
246
275
|
jsx("td", { style: tdR, children: fmt(r.input) }),
|
|
247
276
|
jsx("td", { style: tdR, children: fmt(r.cacheRead) }),
|
|
248
|
-
jsx("td", { style: tdR, children: fmt(r.output) })
|
|
277
|
+
jsx("td", { style: tdR, children: fmt(r.output) }),
|
|
278
|
+
jsx("td", { style: { ...tdR, color: "var(--dsw-alias-label-primary)", fontWeight: 600 }, children: "¥" + fmtMoney(r.cost || 0) })
|
|
249
279
|
] }, r.key)) })
|
|
250
280
|
] });
|
|
251
281
|
|
|
@@ -263,6 +293,15 @@ window.__ModuleLoader__.load({
|
|
|
263
293
|
jsx("div", { style: SEC_TITLE, children: "📊 Token 使用量" }),
|
|
264
294
|
statBar,
|
|
265
295
|
jsxs("div", { style: { ...ROW, marginTop: "10px" }, children: [
|
|
296
|
+
jsx("span", { children: "今日花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.todayCost) })
|
|
297
|
+
] }),
|
|
298
|
+
jsxs("div", { style: ROW, children: [
|
|
299
|
+
jsx("span", { children: "本月花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.thisMonthCost) })
|
|
300
|
+
] }),
|
|
301
|
+
jsxs("div", { style: ROW, children: [
|
|
302
|
+
jsx("span", { children: "总花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.cost) })
|
|
303
|
+
] }),
|
|
304
|
+
jsxs("div", { style: { ...ROW, marginTop: "6px", borderTop: "1px solid var(--dsw-alias-border-l1)", paddingTop: "6px" }, children: [
|
|
266
305
|
jsx("span", { children: "请求数" }), jsx("span", { ...MONO, children: fmt(req) })
|
|
267
306
|
] }),
|
|
268
307
|
jsxs("div", { style: ROW, children: [
|
|
@@ -291,6 +330,15 @@ window.__ModuleLoader__.load({
|
|
|
291
330
|
heatGrid
|
|
292
331
|
] }),
|
|
293
332
|
|
|
333
|
+
// Trend chart
|
|
334
|
+
jsxs("div", { style: CARD, children: [
|
|
335
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }, children: [
|
|
336
|
+
jsx("div", { style: SEC_TITLE, children: "📉 趋势" }),
|
|
337
|
+
jsx(Seg, { options: [{ label: "成本", value: "cost" }, { label: "Token", value: "token" }], value: metric, onChange: setMetric })
|
|
338
|
+
] }),
|
|
339
|
+
trendSvg
|
|
340
|
+
] }),
|
|
341
|
+
|
|
294
342
|
// Models table
|
|
295
343
|
jsxs("div", { style: CARD, children: [
|
|
296
344
|
jsx("div", { style: SEC_TITLE, children: "🤖 按模型" }),
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,51 @@ import { once } from "node:events";
|
|
|
21
21
|
|
|
22
22
|
const ZSTD_MAGIC = 0xfd2fb528; // bytes 28 b5 2f fd (little-endian)
|
|
23
23
|
|
|
24
|
+
// ---- DeepSeek V4 pricing (CNY per 1,000,000 tokens) ----
|
|
25
|
+
// Source: https://api-docs.deepseek.com/zh-cn/quick_start/pricing
|
|
26
|
+
// Prices are peak/off-peak. Off-peak = half of peak. Peak = Mon–Fri 09:00–12:00, 14:00–18:00.
|
|
27
|
+
// deepseek-v4-flash and deepseek-v4-flash-vision-exp share the same price.
|
|
28
|
+
const MODEL_PRICES = {
|
|
29
|
+
"deepseek-v4-flash": { cacheHit: [0.05, 0.10], cacheMiss: [1.5, 3.0], output: [4.5, 9.0] },
|
|
30
|
+
"deepseek-v4-flash-vision-exp": { cacheHit: [0.05, 0.10], cacheMiss: [1.5, 3.0], output: [4.5, 9.0] },
|
|
31
|
+
"deepseek-v4-pro": { cacheHit: [0.15, 0.30], cacheMiss: [4.5, 9.0], output: [13.5, 27.0] }
|
|
32
|
+
};
|
|
33
|
+
// Fallback for any model not in the table (use flash's price).
|
|
34
|
+
const FALLBACK_PRICE = MODEL_PRICES["deepseek-v4-flash"];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the given timestamp falls in DeepSeek's peak period.
|
|
38
|
+
* Peak = Mon–Fri 09:00–12:00 and 14:00–18:00 (Beijing time / local device time).
|
|
39
|
+
* @param {number|null} timeMs - epoch milliseconds of the event.
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isPeakTime(timeMs) {
|
|
43
|
+
if (!timeMs) return false;
|
|
44
|
+
const d = new Date(timeMs);
|
|
45
|
+
const dow = d.getDay(); // 0=Sun .. 6=Sat
|
|
46
|
+
if (dow === 0 || dow === 6) return false; // weekends are off-peak
|
|
47
|
+
const mins = d.getHours() * 60 + d.getMinutes();
|
|
48
|
+
// 09:00-12:00 => 540-720 ; 14:00-18:00 => 840-1080
|
|
49
|
+
return (mins >= 540 && mins < 720) || (mins >= 840 && mins < 1080);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute the CNY cost of one usage event given its token breakdown + timestamp.
|
|
54
|
+
* @param {{input:number, output:number, cacheRead:number}} t
|
|
55
|
+
* @param {number|null} timeMs
|
|
56
|
+
* @param {string} model
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
function costOf(t, timeMs, model) {
|
|
60
|
+
const p = MODEL_PRICES[model] || FALLBACK_PRICE;
|
|
61
|
+
const peak = isPeakTime(timeMs) ? 1 : 0;
|
|
62
|
+
// cacheRead is charged at the cache-hit input price; reasoning tokens are billed
|
|
63
|
+
// as output per DeepSeek's model—fold reasoning into output for cost.
|
|
64
|
+
const inputCost = (t.cacheRead * p.cacheHit[peak] + (t.input) * p.cacheMiss[peak]) / 1e6;
|
|
65
|
+
const outputCost = ((t.output + t.reasoning || 0) * p.output[peak]) / 1e6;
|
|
66
|
+
return inputCost + outputCost;
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
/**
|
|
25
70
|
* Split a zstd buffer into its constituent frames. DSH writes multi-frame zstd
|
|
26
71
|
* (one frame per chunk). We walk frame headers to find each frame boundary by
|
|
@@ -95,7 +140,7 @@ function zstdDecompressToText(buf) {
|
|
|
95
140
|
}
|
|
96
141
|
|
|
97
142
|
/** Normalize a token-usage event into our source-neutral shape. */
|
|
98
|
-
function usagesFromEvent(obj, acc, currentModel) {
|
|
143
|
+
function usagesFromEvent(obj, acc, currentModel, timeMs) {
|
|
99
144
|
if (!obj || typeof obj !== "object") return;
|
|
100
145
|
const data = obj.data;
|
|
101
146
|
if (!data) return;
|
|
@@ -122,13 +167,14 @@ function usagesFromEvent(obj, acc, currentModel) {
|
|
|
122
167
|
const model = chunk.model || data.model || obj.model || currentModel || "unknown";
|
|
123
168
|
if (model) {
|
|
124
169
|
let m = acc.byModel[model];
|
|
125
|
-
if (m === undefined) { m = acc.byModel[model] = { req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0 }; }
|
|
170
|
+
if (m === undefined) { m = acc.byModel[model] = { req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0, cost: 0 }; }
|
|
126
171
|
m.req += 1;
|
|
127
172
|
m.input += input;
|
|
128
173
|
m.output += output;
|
|
129
174
|
m.cacheRead += cacheRead;
|
|
130
175
|
m.reasoning += reasoning;
|
|
131
176
|
m.total += sum;
|
|
177
|
+
m.cost += costOf({ input, output, cacheRead, reasoning }, timeMs, model);
|
|
132
178
|
}
|
|
133
179
|
}
|
|
134
180
|
|
|
@@ -156,7 +202,7 @@ function findSessionLogs(root) {
|
|
|
156
202
|
/** Aggregate token stats across all session logs under `root`. */
|
|
157
203
|
function aggregateTokens(root) {
|
|
158
204
|
const acc = {
|
|
159
|
-
total: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0,
|
|
205
|
+
total: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, cost: 0,
|
|
160
206
|
events: 0, req: 0, cacheHits: 0, sessions: 0, files: 0,
|
|
161
207
|
byModel: {}, byDay: {}, byWorkspace: {}
|
|
162
208
|
};
|
|
@@ -204,22 +250,32 @@ function aggregateTokens(root) {
|
|
|
204
250
|
const sum = inTok + outTok + cTok + rTok;
|
|
205
251
|
const t = o.time ?? o.createdAt;
|
|
206
252
|
const day = t ? new Date(t).toISOString().slice(0, 10) : null;
|
|
253
|
+
// cost of this single event (peak/off-peak aware, model-aware)
|
|
254
|
+
const cost = costOf({ input: inTok, output: outTok, cacheRead: cTok, reasoning: rTok }, t, currentModel || "unknown");
|
|
255
|
+
acc.cost += cost;
|
|
207
256
|
if (day !== null) {
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
257
|
+
let d = acc.byDay[day];
|
|
258
|
+
if (d === undefined) { d = acc.byDay[day] = { total: 0, input: 0, output: 0, cacheRead: 0, cost: 0 }; }
|
|
259
|
+
d.total += sum;
|
|
260
|
+
d.input += inTok;
|
|
261
|
+
d.output += outTok;
|
|
262
|
+
d.cacheRead += cTok;
|
|
263
|
+
d.cost += cost;
|
|
264
|
+
if (day === todayKey) { acc.today = (acc.today || 0) + sum; acc.todayCost = (acc.todayCost || 0) + cost; }
|
|
265
|
+
if (day.slice(0, 7) === monthKey) { acc.thisMonth = (acc.thisMonth || 0) + sum; acc.thisMonthCost = (acc.thisMonthCost || 0) + cost; }
|
|
211
266
|
}
|
|
212
267
|
// per-workspace broken-down counters
|
|
213
268
|
let w = acc.byWorkspace[ws];
|
|
214
|
-
if (w === undefined) { w = acc.byWorkspace[ws] = { req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0 }; }
|
|
269
|
+
if (w === undefined) { w = acc.byWorkspace[ws] = { req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0, cost: 0 }; }
|
|
215
270
|
w.req += 1;
|
|
216
271
|
w.input += inTok;
|
|
217
272
|
w.output += outTok;
|
|
218
273
|
w.cacheRead += cTok;
|
|
219
274
|
w.reasoning += rTok;
|
|
220
275
|
w.total += sum;
|
|
276
|
+
w.cost += cost;
|
|
221
277
|
}
|
|
222
|
-
usagesFromEvent(o, acc, currentModel);
|
|
278
|
+
usagesFromEvent(o, acc, currentModel, o.time ?? o.createdAt);
|
|
223
279
|
}
|
|
224
280
|
}
|
|
225
281
|
return acc;
|
package/package.json
CHANGED
|
@@ -1,49 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lyk308/dsh-token-dashboard",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DeepSeek Harness 实时看板:账户余额 + Token 使用量(按模型/工作区/缓存命中),本地读取会话日志 + DeepSeek /user/balance。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./client": "./lib/client.js",
|
|
10
|
-
"./package.json": "./package.json"
|
|
11
|
-
},
|
|
12
|
-
"dsh": {
|
|
13
|
-
"bundle": {
|
|
14
|
-
"patch": "./cordis.patch.yml"
|
|
15
|
-
},
|
|
16
|
-
"client": {
|
|
17
|
-
"inject": [
|
|
18
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
19
|
-
"@deepseek-ai/dsh-client-locale",
|
|
20
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
21
|
-
"@deepseek-ai/dsh-client-ui-primitives"
|
|
22
|
-
],
|
|
23
|
-
"platform": "web"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"lib/index.js",
|
|
28
|
-
"lib/client.js",
|
|
29
|
-
"cordis.patch.yml"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
],
|
|
43
|
-
"license": "MIT",
|
|
44
|
-
"repository": {
|
|
45
|
-
"type": "git",
|
|
46
|
-
"url": "https://github.com/lyk308/dsh-token-dashboard.git"
|
|
47
|
-
},
|
|
48
|
-
"homepage": "https://github.com/lyk308/dsh-token-dashboard"
|
|
49
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@lyk308/dsh-token-dashboard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DeepSeek Harness 实时看板:账户余额 + Token 使用量(按模型/工作区/缓存命中),本地读取会话日志 + DeepSeek /user/balance。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"inject": [
|
|
18
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
19
|
+
"@deepseek-ai/dsh-client-locale",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
21
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
22
|
+
],
|
|
23
|
+
"platform": "web"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"lib/index.js",
|
|
28
|
+
"lib/client.js",
|
|
29
|
+
"cordis.patch.yml"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"deepseek-harness",
|
|
33
|
+
"dsh",
|
|
34
|
+
"plugin",
|
|
35
|
+
"token",
|
|
36
|
+
"balance",
|
|
37
|
+
"usage",
|
|
38
|
+
"dashboard"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|