token-stats-timer 1.0.16 → 1.0.17
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 +2 -1
- package/package.json +1 -1
- package/token-stats.ts +154 -5
package/README.md
CHANGED
|
@@ -76,8 +76,9 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
76
76
|
|
|
77
77
|
## 命令
|
|
78
78
|
|
|
79
|
-
- `/stats` ——
|
|
79
|
+
- `/stats` —— 无参默认显示**当天 token 统计**(等价 `/stats day`)
|
|
80
80
|
- `/stats day [YYYY-MM-DD]` / `hour` / `week` / `month [YYYY-MM]` —— 统计查询
|
|
81
|
+
- `/stats limit` —— 套餐配置(为当前 provider 选择/关闭配额套餐;选 GLM 后会继续询问是否配置团队套餐凭证)
|
|
81
82
|
- `/stats config` —— 显示样式 / 显示内容 / 配额刷新时间 / GLM 团队凭证
|
|
82
83
|
- `/notify [on|off|test]` —— 通知开关 / 测试(无参查看状态)
|
|
83
84
|
|
package/package.json
CHANGED
package/token-stats.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
appendFile,
|
|
21
21
|
mkdir,
|
|
22
22
|
readFile,
|
|
23
|
+
readdir,
|
|
23
24
|
writeFile,
|
|
24
25
|
} from "node:fs/promises";
|
|
25
26
|
import { join } from "node:path";
|
|
@@ -249,6 +250,134 @@ function renderTable(
|
|
|
249
250
|
return lines;
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
// ── 按模型统计(读取 raw 明细,按 model 聚合)────────────────────
|
|
254
|
+
|
|
255
|
+
/** 读取若干日期的原始逐条记录 */
|
|
256
|
+
async function readRawRecordsForDates(dates: string[]): Promise<RawRecord[]> {
|
|
257
|
+
const out: RawRecord[] = [];
|
|
258
|
+
for (const d of dates) {
|
|
259
|
+
try {
|
|
260
|
+
const content = await readFile(join(RAW_DIR, `${d}.jsonl`), "utf-8");
|
|
261
|
+
for (const line of content.trim().split("\n")) {
|
|
262
|
+
if (line) out.push(JSON.parse(line));
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// 该日无原始数据
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 读取 [startDate, endDate] 闭区间内所有日期的原始记录 */
|
|
272
|
+
async function readRawRecordsInRange(
|
|
273
|
+
startDate: string,
|
|
274
|
+
endDate: string,
|
|
275
|
+
): Promise<RawRecord[]> {
|
|
276
|
+
const dates: string[] = [];
|
|
277
|
+
try {
|
|
278
|
+
const files = await readdir(RAW_DIR);
|
|
279
|
+
for (const f of files) {
|
|
280
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
281
|
+
const d = f.slice(0, 10);
|
|
282
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= startDate && d <= endDate) {
|
|
283
|
+
dates.push(d);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// RAW_DIR 不存在
|
|
288
|
+
}
|
|
289
|
+
return readRawRecordsForDates(dates);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface ModelAgg {
|
|
293
|
+
count: number;
|
|
294
|
+
input: number;
|
|
295
|
+
cacheRead: number;
|
|
296
|
+
cacheWrite: number;
|
|
297
|
+
output: number;
|
|
298
|
+
tokensPerSecSum: number;
|
|
299
|
+
hitRateSum: number;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 按模型生成 markdown 用量表(模型行按总token降序,末尾合计) */
|
|
303
|
+
function renderModelBreakdown(records: RawRecord[]): string[] {
|
|
304
|
+
if (records.length === 0) return ["", "> 按模型:该范围暂无明细数据"];
|
|
305
|
+
|
|
306
|
+
const byModel = new Map<string, ModelAgg>();
|
|
307
|
+
for (const r of records) {
|
|
308
|
+
const key = r.model || "unknown";
|
|
309
|
+
const agg = byModel.get(key) ?? {
|
|
310
|
+
count: 0,
|
|
311
|
+
input: 0,
|
|
312
|
+
cacheRead: 0,
|
|
313
|
+
cacheWrite: 0,
|
|
314
|
+
output: 0,
|
|
315
|
+
tokensPerSecSum: 0,
|
|
316
|
+
hitRateSum: 0,
|
|
317
|
+
};
|
|
318
|
+
agg.count++;
|
|
319
|
+
agg.input += r.input;
|
|
320
|
+
agg.cacheRead += r.cacheRead;
|
|
321
|
+
agg.cacheWrite += r.cacheWrite;
|
|
322
|
+
agg.output += r.output;
|
|
323
|
+
agg.tokensPerSecSum += r.tokensPerSec;
|
|
324
|
+
agg.hitRateSum += r.cacheHitRate;
|
|
325
|
+
byModel.set(key, agg);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 与汇总口径一致:总token = 新增输入 + 缓存输入
|
|
329
|
+
const totalTokensOf = (a: ModelAgg) => a.input + a.cacheRead + a.cacheWrite;
|
|
330
|
+
const rows = [...byModel.entries()].sort(
|
|
331
|
+
(a, b) => totalTokensOf(b[1]) - totalTokensOf(a[1]),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const total: ModelAgg = {
|
|
335
|
+
count: 0, input: 0, cacheRead: 0, cacheWrite: 0,
|
|
336
|
+
output: 0, tokensPerSecSum: 0, hitRateSum: 0,
|
|
337
|
+
};
|
|
338
|
+
const body: string[][] = rows.map(([model, agg]) => {
|
|
339
|
+
total.count += agg.count;
|
|
340
|
+
total.input += agg.input;
|
|
341
|
+
total.cacheRead += agg.cacheRead;
|
|
342
|
+
total.cacheWrite += agg.cacheWrite;
|
|
343
|
+
total.output += agg.output;
|
|
344
|
+
total.tokensPerSecSum += agg.tokensPerSecSum;
|
|
345
|
+
total.hitRateSum += agg.hitRateSum;
|
|
346
|
+
return [
|
|
347
|
+
model,
|
|
348
|
+
String(agg.count),
|
|
349
|
+
formatTokens(agg.input),
|
|
350
|
+
formatTokens(agg.cacheRead),
|
|
351
|
+
formatTokens(agg.output),
|
|
352
|
+
formatTokens(totalTokensOf(agg)),
|
|
353
|
+
`${(agg.count > 0 ? agg.hitRateSum / agg.count : 0).toFixed(1)}%`,
|
|
354
|
+
`${(agg.count > 0 ? agg.tokensPerSecSum / agg.count : 0).toFixed(1)}`,
|
|
355
|
+
];
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return [
|
|
359
|
+
"",
|
|
360
|
+
"**按模型**",
|
|
361
|
+
...renderTable(
|
|
362
|
+
["模型", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
|
|
363
|
+
body,
|
|
364
|
+
{
|
|
365
|
+
aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
|
|
366
|
+
totalRow: [
|
|
367
|
+
"合计",
|
|
368
|
+
String(total.count),
|
|
369
|
+
formatTokens(total.input),
|
|
370
|
+
formatTokens(total.cacheRead),
|
|
371
|
+
formatTokens(total.output),
|
|
372
|
+
formatTokens(total.input + total.cacheRead + total.cacheWrite),
|
|
373
|
+
`${(total.count > 0 ? total.hitRateSum / total.count : 0).toFixed(1)}%`,
|
|
374
|
+
`${(total.count > 0 ? total.tokensPerSecSum / total.count : 0).toFixed(1)}`,
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
),
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
252
381
|
function isReasonableTokenSpeed(tokensPerSecond: number): boolean {
|
|
253
382
|
return Number.isFinite(tokensPerSecond) && tokensPerSecond > 0 && tokensPerSecond <= MAX_REASONABLE_TOKEN_SPEED;
|
|
254
383
|
}
|
|
@@ -1478,7 +1607,7 @@ export function createTokenStats(
|
|
|
1478
1607
|
}
|
|
1479
1608
|
|
|
1480
1609
|
await showStats(
|
|
1481
|
-
renderDaySummary(daily),
|
|
1610
|
+
[...renderDaySummary(daily), ...renderModelBreakdown(await readRawRecordsForDates([date]))],
|
|
1482
1611
|
`Token 统计 | ${date}`,
|
|
1483
1612
|
ctx,
|
|
1484
1613
|
);
|
|
@@ -1562,7 +1691,11 @@ export function createTokenStats(
|
|
|
1562
1691
|
},
|
|
1563
1692
|
);
|
|
1564
1693
|
|
|
1565
|
-
await showStats(
|
|
1694
|
+
await showStats(
|
|
1695
|
+
[...lines, ...renderModelBreakdown(await readRawRecordsInRange(sevenDaysAgo, today))],
|
|
1696
|
+
"本周每天汇总",
|
|
1697
|
+
ctx,
|
|
1698
|
+
);
|
|
1566
1699
|
}
|
|
1567
1700
|
|
|
1568
1701
|
function getMonthStr(date: Date = new Date()): string {
|
|
@@ -1636,7 +1769,17 @@ export function createTokenStats(
|
|
|
1636
1769
|
},
|
|
1637
1770
|
);
|
|
1638
1771
|
|
|
1639
|
-
|
|
1772
|
+
const monthDates = monthRecords.map((r) => r.date).sort();
|
|
1773
|
+
await showStats(
|
|
1774
|
+
[
|
|
1775
|
+
...lines,
|
|
1776
|
+
...renderModelBreakdown(
|
|
1777
|
+
await readRawRecordsInRange(monthDates[0], monthDates[monthDates.length - 1]),
|
|
1778
|
+
),
|
|
1779
|
+
],
|
|
1780
|
+
`${month} 月度汇总`,
|
|
1781
|
+
ctx,
|
|
1782
|
+
);
|
|
1640
1783
|
}
|
|
1641
1784
|
|
|
1642
1785
|
// ── 事件注册 ─────────────────────────────────────────
|
|
@@ -1857,12 +2000,18 @@ export function createTokenStats(
|
|
|
1857
2000
|
// ── /stats 命令 ─────────────────────────────────────
|
|
1858
2001
|
|
|
1859
2002
|
pi.registerCommand("stats", {
|
|
1860
|
-
description: "Token 统计 (day | hour | week | month | config)
|
|
2003
|
+
description: "Token 统计 (day | hour | week | month | config | limit) 无参默认显示当天统计;limit 进入套餐配置",
|
|
1861
2004
|
handler: async (args, ctx) => {
|
|
1862
2005
|
const arg = args.trim();
|
|
1863
2006
|
|
|
1864
|
-
// 无参 →
|
|
2007
|
+
// 无参 → 当天统计(等价于 /stats day)
|
|
1865
2008
|
if (!arg) {
|
|
2009
|
+
await showDay(getDateStr(), ctx);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// limit → 套餐配置(原无参行为)
|
|
2014
|
+
if (arg === "limit") {
|
|
1866
2015
|
const provider = ctx.model?.provider;
|
|
1867
2016
|
if (!provider) {
|
|
1868
2017
|
ctx.ui.notify("无法获取当前供应商,请先切换对话", "warning");
|