@lyk308/dsh-token-dashboard 0.1.3 → 0.1.4
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 +69 -64
- package/README.zh.md +69 -64
- package/docs/screenshot.png +0 -0
- package/docs/screenshot2.png +0 -0
- package/lib/client.js +561 -393
- package/lib/index.js +73 -43
- package/package.json +45 -45
package/lib/index.js
CHANGED
|
@@ -139,8 +139,11 @@ function zstdDecompressToText(buf) {
|
|
|
139
139
|
return Buffer.concat(parts).toString("utf8");
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/** Normalize a token-usage event into our source-neutral shape.
|
|
143
|
-
|
|
142
|
+
/** Normalize a token-usage event into our source-neutral shape.
|
|
143
|
+
* @param {boolean} inRange - false skips aggregation (used for window filtering).
|
|
144
|
+
*/
|
|
145
|
+
function usagesFromEvent(obj, acc, currentModel, timeMs, inRange) {
|
|
146
|
+
if (inRange === false) return;
|
|
144
147
|
if (!obj || typeof obj !== "object") return;
|
|
145
148
|
const data = obj.data;
|
|
146
149
|
if (!data) return;
|
|
@@ -199,24 +202,37 @@ function findSessionLogs(root) {
|
|
|
199
202
|
return out;
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
/** Aggregate token stats across all session logs under `root`.
|
|
203
|
-
|
|
205
|
+
/** Aggregate token stats across all session logs under `root`.
|
|
206
|
+
* @param {string} root - sessions directory.
|
|
207
|
+
* @param {number|null} days - if set, byModel/byWorkspace/byDay/bySession are
|
|
208
|
+
* filtered to events within the last `days` days, while total/cost/today/
|
|
209
|
+
* thisMonth remain full-window (those are cardinal period facts).
|
|
210
|
+
*/
|
|
211
|
+
function aggregateTokens(root, days) {
|
|
204
212
|
const acc = {
|
|
205
213
|
total: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, cost: 0,
|
|
206
214
|
events: 0, req: 0, cacheHits: 0, sessions: 0, files: 0,
|
|
207
|
-
byModel: {}, byDay: {}, byWorkspace: {}
|
|
215
|
+
byModel: {}, byDay: {}, byWorkspace: {}, bySession: {}
|
|
208
216
|
};
|
|
209
217
|
const files = findSessionLogs(root);
|
|
210
218
|
acc.files = files.length;
|
|
211
219
|
const now = new Date();
|
|
212
220
|
const todayKey = now.toISOString().slice(0, 10);
|
|
213
221
|
const monthKey = todayKey.slice(0, 7);
|
|
222
|
+
// Range cutoff (epoch ms): events older than this are excluded from the
|
|
223
|
+
// per-group aggregates when `days` is provided. null = no filtering.
|
|
224
|
+
const cutoff = (days && days > 0) ? Date.now() - days * 86400 * 1000 : null;
|
|
214
225
|
for (const file of files) {
|
|
215
226
|
let bytes;
|
|
216
227
|
try { bytes = readFileSync(file); } catch { continue; }
|
|
217
228
|
const text = file.endsWith(".zstd") ? zstdDecompressToText(bytes) : bytes.toString("utf8");
|
|
218
229
|
if (!text) continue;
|
|
219
230
|
acc.sessions += 1;
|
|
231
|
+
// Session id: the parent directory name of each session.jsonl[.zstd].
|
|
232
|
+
// Path shape: .../sessions/<workspace>/<session-id>/session.jsonl[.zstd]
|
|
233
|
+
let sessionId = basename(dirname(file));
|
|
234
|
+
let sRec = acc.bySession[sessionId];
|
|
235
|
+
if (sRec === undefined) { sRec = acc.bySession[sessionId] = { id: sessionId, total: 0, cost: 0, req: 0, first: null, last: null, model: null, workspace: "unknown" }; }
|
|
220
236
|
const lines = text.split("\n");
|
|
221
237
|
let ws = "unknown";
|
|
222
238
|
let currentModel = null;
|
|
@@ -224,7 +240,7 @@ function aggregateTokens(root) {
|
|
|
224
240
|
if (!line) continue;
|
|
225
241
|
let o;
|
|
226
242
|
try { o = JSON.parse(line); } catch { continue; }
|
|
227
|
-
if (o.type === "session" && o.cwd) ws = o.cwd;
|
|
243
|
+
if (o.type === "session" && o.cwd) { ws = o.cwd; sRec.workspace = ws; const t0 = o.createdAt; if (t0 && (sRec.first === null || t0 < sRec.first)) sRec.first = t0; }
|
|
228
244
|
// Track the active model. DSH records it on:
|
|
229
245
|
// - request/header: data.header.config.model (authoritative)
|
|
230
246
|
// - request/context: data.model
|
|
@@ -232,6 +248,7 @@ function aggregateTokens(root) {
|
|
|
232
248
|
if (o.type === "request/header") {
|
|
233
249
|
const m = o.data?.header?.config?.model || o.data?.model || null;
|
|
234
250
|
if (m) currentModel = m;
|
|
251
|
+
if (m && (sRec.model === null || currentModel)) sRec.model = m;
|
|
235
252
|
} else if (o.type === "assistant/message") {
|
|
236
253
|
const m = o.data?.usage?.model || o.data?.model || null;
|
|
237
254
|
if (m) currentModel = m;
|
|
@@ -239,6 +256,10 @@ function aggregateTokens(root) {
|
|
|
239
256
|
const m = o.data?.model || null;
|
|
240
257
|
if (m) currentModel = m;
|
|
241
258
|
}
|
|
259
|
+
// Whether this event falls within the requested window (computed once so
|
|
260
|
+
// both the usageBlock branch and usagesFromEvent can use it).
|
|
261
|
+
const evT = o.time ?? o.createdAt;
|
|
262
|
+
const inRange = cutoff === null || (evT && evT >= cutoff);
|
|
242
263
|
// Per-day + per-workspace accumulation must happen per event BEFORE the
|
|
243
264
|
// generic usagesFromEvent bump, so we mirror the same broken-down shape.
|
|
244
265
|
const usageBlock = o.data?.chunk?.usage;
|
|
@@ -252,8 +273,9 @@ function aggregateTokens(root) {
|
|
|
252
273
|
const day = t ? new Date(t).toISOString().slice(0, 10) : null;
|
|
253
274
|
// cost of this single event (peak/off-peak aware, model-aware)
|
|
254
275
|
const cost = costOf({ input: inTok, output: outTok, cacheRead: cTok, reasoning: rTok }, t, currentModel || "unknown");
|
|
255
|
-
|
|
256
|
-
if (
|
|
276
|
+
// Whether this event is within the requested window (reuse outer inRange).
|
|
277
|
+
if (inRange) acc.cost += cost;
|
|
278
|
+
if (day !== null && inRange) {
|
|
257
279
|
let d = acc.byDay[day];
|
|
258
280
|
if (d === undefined) { d = acc.byDay[day] = { total: 0, input: 0, output: 0, cacheRead: 0, cost: 0 }; }
|
|
259
281
|
d.total += sum;
|
|
@@ -261,21 +283,33 @@ function aggregateTokens(root) {
|
|
|
261
283
|
d.output += outTok;
|
|
262
284
|
d.cacheRead += cTok;
|
|
263
285
|
d.cost += cost;
|
|
286
|
+
}
|
|
287
|
+
// today / thisMonth are cardinal period facts (全量, independent of window)
|
|
288
|
+
if (day !== null) {
|
|
264
289
|
if (day === todayKey) { acc.today = (acc.today || 0) + sum; acc.todayCost = (acc.todayCost || 0) + cost; }
|
|
265
290
|
if (day.slice(0, 7) === monthKey) { acc.thisMonth = (acc.thisMonth || 0) + sum; acc.thisMonthCost = (acc.thisMonthCost || 0) + cost; }
|
|
266
291
|
}
|
|
267
292
|
// per-workspace broken-down counters
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
293
|
+
if (inRange) {
|
|
294
|
+
let w = acc.byWorkspace[ws];
|
|
295
|
+
if (w === undefined) { w = acc.byWorkspace[ws] = { req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0, cost: 0 }; }
|
|
296
|
+
w.req += 1;
|
|
297
|
+
w.input += inTok;
|
|
298
|
+
w.output += outTok;
|
|
299
|
+
w.cacheRead += cTok;
|
|
300
|
+
w.reasoning += rTok;
|
|
301
|
+
w.total += sum;
|
|
302
|
+
w.cost += cost;
|
|
303
|
+
// per-session accumulation
|
|
304
|
+
sRec.total += sum;
|
|
305
|
+
sRec.cost += cost;
|
|
306
|
+
sRec.req += 1;
|
|
307
|
+
sRec.model = currentModel || sRec.model;
|
|
308
|
+
const evTime = o.time ?? o.createdAt;
|
|
309
|
+
if (evTime) { if (sRec.first === null || evTime < sRec.first) sRec.first = evTime; if (sRec.last === null || evTime > sRec.last) sRec.last = evTime; }
|
|
310
|
+
}
|
|
277
311
|
}
|
|
278
|
-
usagesFromEvent(o, acc, currentModel, o.time ?? o.createdAt);
|
|
312
|
+
usagesFromEvent(o, acc, currentModel, o.time ?? o.createdAt, inRange);
|
|
279
313
|
}
|
|
280
314
|
}
|
|
281
315
|
return acc;
|
|
@@ -328,24 +362,10 @@ export function apply(ctx, config) {
|
|
|
328
362
|
// blank" symptom. Caching makes the request path O(1) and defers the costly
|
|
329
363
|
// scan to a background refresh that never blocks a response.
|
|
330
364
|
const STATS_TTL_MS = 60_000;
|
|
331
|
-
|
|
365
|
+
// Cache keyed by window (days): { [days]: { at, payload, computing } }.
|
|
366
|
+
let statsCache = {};
|
|
332
367
|
|
|
333
|
-
|
|
334
|
-
if (statsCache.computing) return; // already running; back off
|
|
335
|
-
statsCache.computing = true;
|
|
336
|
-
// Defer to the next tick so the current handler / event loop is not held.
|
|
337
|
-
setImmediate(() => {
|
|
338
|
-
try {
|
|
339
|
-
const s = aggregateTokens(sessionsRoot);
|
|
340
|
-
statsCache = { at: Date.now(), payload: s, computing: false };
|
|
341
|
-
} catch (error) {
|
|
342
|
-
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
343
|
-
statsCache = { ...statsCache, computing: false };
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
// Warm the cache once on boot so the first open returns data immediately.
|
|
348
|
-
scheduleStatsRefresh();
|
|
368
|
+
// (stats cache is filled lazily on the first /stats request, synchronously.)
|
|
349
369
|
|
|
350
370
|
// --- route: token stats (served from cache; never blocks the event loop) ---
|
|
351
371
|
ctx.webServer.register({
|
|
@@ -355,16 +375,26 @@ export function apply(ctx, config) {
|
|
|
355
375
|
try {
|
|
356
376
|
const url = new URL(req.url ?? "/", "http://x");
|
|
357
377
|
const force = url.searchParams.get("refresh") === "1";
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
378
|
+
const days = Number(url.searchParams.get("days"));
|
|
379
|
+
const winDays = (days > 0) ? days : null;
|
|
380
|
+
const key = winDays || 0;
|
|
381
|
+
let cell = statsCache[key];
|
|
382
|
+
// Serve from cache if fresh; otherwise compute synchronously (first call
|
|
383
|
+
// after a window switch blocks ~1.5s to fill the cache, then it's instant).
|
|
384
|
+
if (force || !cell || (Date.now() - cell.at > STATS_TTL_MS && !cell.computing)) {
|
|
385
|
+
const s = aggregateTokens(sessionsRoot, winDays);
|
|
386
|
+
statsCache[key] = { at: Date.now(), payload: s, computing: false };
|
|
387
|
+
cell = statsCache[key];
|
|
388
|
+
}
|
|
389
|
+
if (cell && cell.payload) {
|
|
390
|
+
sendJson(res, 200, { ...cell.payload, cached: true, windowDays: winDays });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (cell && cell.error) {
|
|
394
|
+
sendJson(res, 500, { status: "error", message: cell.error, windowDays: winDays });
|
|
363
395
|
return;
|
|
364
396
|
}
|
|
365
|
-
|
|
366
|
-
// immediately with a "computing" marker instead of blocking on the scan.
|
|
367
|
-
sendJson(res, 200, { status: "computing", cached: false });
|
|
397
|
+
sendJson(res, 200, { status: "computing", cached: false, windowDays: winDays });
|
|
368
398
|
} catch (error) {
|
|
369
399
|
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
370
400
|
sendJson(res, 500, { status: "error", message: String(error) });
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
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
|
-
"README.md",
|
|
31
|
-
"README.zh.md",
|
|
32
|
-
"LICENSE",
|
|
33
|
-
"docs"
|
|
34
|
-
],
|
|
35
|
-
"keywords": [
|
|
36
|
-
"deepseek-harness",
|
|
37
|
-
"dsh",
|
|
38
|
-
"plugin",
|
|
39
|
-
"token",
|
|
40
|
-
"balance",
|
|
41
|
-
"usage",
|
|
42
|
-
"dashboard"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@lyk308/dsh-token-dashboard",
|
|
3
|
+
"version": "0.1.4",
|
|
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
|
+
"README.md",
|
|
31
|
+
"README.zh.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"docs"
|
|
34
|
+
],
|
|
35
|
+
"keywords": [
|
|
36
|
+
"deepseek-harness",
|
|
37
|
+
"dsh",
|
|
38
|
+
"plugin",
|
|
39
|
+
"token",
|
|
40
|
+
"balance",
|
|
41
|
+
"usage",
|
|
42
|
+
"dashboard"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|