token-stats-timer 1.0.16 → 1.0.18

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/token-stats.ts +172 -16
package/README.md CHANGED
@@ -76,8 +76,9 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
76
76
 
77
77
  ## 命令
78
78
 
79
- - `/stats` —— 无参进入套餐配置(为当前 provider 选择/关闭配额套餐;选 GLM 后会继续询问是否配置团队套餐凭证)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-stats-timer",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "pi extension to display token usage and time spend.",
5
5
  "keywords": [
6
6
  "pi-package",
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";
@@ -195,7 +196,7 @@ type QuotaError =
195
196
  * Token 格式化(对齐 @firstpick/pi-utils formatTokens)
196
197
  */
197
198
  function formatTokens(count: number): string {
198
- if (count < 1000) return count.toString();
199
+ if (count < 1000) return count.toFixed(1);
199
200
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
200
201
  if (count < 1000000) return `${Math.round(count / 1000)}k`;
201
202
  if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
@@ -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
  );
@@ -1503,16 +1632,23 @@ export function createTokenStats(
1503
1632
  records.sort((a, b) => a.hour - b.hour);
1504
1633
 
1505
1634
  const lines = renderTable(
1506
- ["时间", "次数", "输入", "输出", "命中率", "速率"],
1507
- records.map((r) => [
1508
- String(r.hour).padStart(2, "0"),
1509
- String(r.count),
1510
- formatTokens(r.sumInput),
1511
- formatTokens(r.sumOutput),
1512
- `${weightedCacheHitRate(r).toFixed(1)}%`,
1513
- `${(r.sumTokensPerSec / r.count).toFixed(1)}`,
1514
- ]),
1515
- { aligns: ["left", "right", "right", "right", "right", "right"] },
1635
+ ["时间", "次数", "新增输入", "缓存输入", "输出", "总token", "命中率", "速率"],
1636
+ records.map((r) => {
1637
+ const totalPrompt = r.sumInput + r.sumCacheRead + r.sumCacheWrite;
1638
+ return [
1639
+ String(r.hour).padStart(2, "0"),
1640
+ String(r.count),
1641
+ formatTokens(r.sumInput),
1642
+ formatTokens(r.sumCacheRead),
1643
+ formatTokens(r.sumOutput),
1644
+ formatTokens(totalPrompt),
1645
+ `${weightedCacheHitRate(r).toFixed(1)}%`,
1646
+ `${(r.sumTokensPerSec / r.count).toFixed(1)}`,
1647
+ ];
1648
+ }),
1649
+ {
1650
+ aligns: ["left", "right", "right", "right", "right", "right", "right", "right"],
1651
+ },
1516
1652
  );
1517
1653
 
1518
1654
  await showStats(lines, `按小时分布 | ${date}`, ctx);
@@ -1562,7 +1698,11 @@ export function createTokenStats(
1562
1698
  },
1563
1699
  );
1564
1700
 
1565
- await showStats(lines, "本周每天汇总", ctx);
1701
+ await showStats(
1702
+ [...lines, ...renderModelBreakdown(await readRawRecordsInRange(sevenDaysAgo, today))],
1703
+ "本周每天汇总",
1704
+ ctx,
1705
+ );
1566
1706
  }
1567
1707
 
1568
1708
  function getMonthStr(date: Date = new Date()): string {
@@ -1636,7 +1776,17 @@ export function createTokenStats(
1636
1776
  },
1637
1777
  );
1638
1778
 
1639
- await showStats(lines, `${month} 月度汇总`, ctx);
1779
+ const monthDates = monthRecords.map((r) => r.date).sort();
1780
+ await showStats(
1781
+ [
1782
+ ...lines,
1783
+ ...renderModelBreakdown(
1784
+ await readRawRecordsInRange(monthDates[0], monthDates[monthDates.length - 1]),
1785
+ ),
1786
+ ],
1787
+ `${month} 月度汇总`,
1788
+ ctx,
1789
+ );
1640
1790
  }
1641
1791
 
1642
1792
  // ── 事件注册 ─────────────────────────────────────────
@@ -1857,12 +2007,18 @@ export function createTokenStats(
1857
2007
  // ── /stats 命令 ─────────────────────────────────────
1858
2008
 
1859
2009
  pi.registerCommand("stats", {
1860
- description: "Token 统计 (day | hour | week | month | config) 无参默认进入套餐配置",
2010
+ description: "Token 统计 (day | hour | week | month | config | limit) 无参默认显示当天统计;limit 进入套餐配置",
1861
2011
  handler: async (args, ctx) => {
1862
2012
  const arg = args.trim();
1863
2013
 
1864
- // 无参 → 套餐配置
2014
+ // 无参 → 当天统计(等价于 /stats day)
1865
2015
  if (!arg) {
2016
+ await showDay(getDateStr(), ctx);
2017
+ return;
2018
+ }
2019
+
2020
+ // limit → 套餐配置(原无参行为)
2021
+ if (arg === "limit") {
1866
2022
  const provider = ctx.model?.provider;
1867
2023
  if (!provider) {
1868
2024
  ctx.ui.notify("无法获取当前供应商,请先切换对话", "warning");