token-stats-timer 1.0.15 → 1.0.16

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 CHANGED
@@ -16,7 +16,7 @@ Footer 上行(左对齐):
16
16
  - `↑ ↓ Σ CH` —— 累计输入 / 输出 / 总量 / 缓存命中率
17
17
  - `⚡` —— 实时速率(2s rolling window,无流时回落到平均速率)
18
18
  - 上下文占用(样式可配)
19
- - `5h: W: ⏱` —— 套餐剩余(MiniMax / GLM / Kimi / DeepSeek 内置套餐,需在 /stats 里为当前 provider 启用)
19
+ - `5h: W: ⏱` —— 套餐剩余(MiniMax / GLM / Kimi / DeepSeek / OpenCode Go 内置套餐,需在 /stats 里为当前 provider 启用)
20
20
 
21
21
  Footer 下行:cwd + git 分支 + 其他扩展状态。
22
22
 
@@ -101,6 +101,12 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
101
101
 
102
102
  > 组织 ID / 项目 ID 在 GLM Coding Plan 团队版后台「团队编程套餐」页面获取;如果你同时使用 Claude Code / Cursor 等工具并已在环境变量里配置,也可以在 config.json 里直接填上同名值。
103
103
 
104
+ ## OpenCode Go 余额
105
+
106
+ OpenCode Go 订阅(`opencode-go` provider,baseUrl `https://opencode.ai/zen/go/v1`)使用官方配额接口 `GET /zen/go/v1/usage`(`Authorization: Bearer <key>`,即 auth.json 里 `opencode-go` 的 `key` 或环境变量 `OPENCODE_API_KEY`),返回三个滚动窗口的已用百分比:5 小时 / 周 / 月。
107
+
108
+ footer 显示 `5h: X% W: Y% M: Z% ⏱ ...`(剩余比例 = 100 - 已用),后三个子项可分别用 `/stats config` → 显示内容 的「5h额度 / 周额度 / 月额度 / 刷新时间」开关控制。
109
+
104
110
  ## 与原包的兼容性
105
111
 
106
112
  - 显示/套餐配置沿用 `~/.pi/agent/extensions/token-stats/`(原 token-stats 的配置直接生效)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-stats-timer",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "pi extension to display token usage and time spend.",
5
5
  "keywords": [
6
6
  "pi-package",
package/token-stats.ts CHANGED
@@ -151,6 +151,7 @@ export type DisplayKey =
151
151
  | "context" // 容量(ctx%)
152
152
  | "quota5h" // 5h 额度
153
153
  | "quotaWeek" // 周额度
154
+ | "quotaMonth" // 月额度(OpenCode Go 等)
154
155
  | "quotaClock" // 刷新时间(⏱)
155
156
 
156
157
  export interface DisplayConfig {
@@ -543,6 +544,73 @@ const BUILTIN_PLANS: TokenPlan[] = [
543
544
  };
544
545
  },
545
546
  },
547
+ {
548
+ id: "opencode-go",
549
+ name: "OpenCode Go",
550
+ matchProviders: ["opencode-go"],
551
+ apiKeyEnv: "OPENCODE_API_KEY",
552
+ baseUrl: "https://opencode.ai",
553
+ quotaPath: "/zen/go/v1/usage",
554
+ authHeader: (key) => ({ Authorization: "Bearer " + key }),
555
+ fetchQuota: async (plan: TokenPlan, key: string) => {
556
+ const r = await fetch(plan.baseUrl + plan.quotaPath, {
557
+ method: "GET",
558
+ headers: {
559
+ Authorization: "Bearer " + key,
560
+ "Content-Type": "application/json",
561
+ },
562
+ signal: AbortSignal.timeout(5000),
563
+ });
564
+ if (!r.ok) throw new Error("OpenCode Go 配额查询 HTTP " + r.status);
565
+ return await r.json();
566
+ },
567
+ format: (data: any) => {
568
+ // 官方 /v1/usage:percent 为已用比例,remaining = 100 - percent
569
+ const u = (data && typeof data === "object" ? (data.usage ?? data) : {}) as any;
570
+ const win = (k: string): any => {
571
+ const w = u?.[k];
572
+ if (w && w.status === "ok" && typeof w.percent === "number") return w;
573
+ return null;
574
+ };
575
+ const rolling = win("rolling");
576
+ const weekly = win("weekly");
577
+ const monthly = win("monthly");
578
+ if (!rolling && !weekly && !monthly) {
579
+ return { modelPrefix: "", display: "无数据", color: "err" as const };
580
+ }
581
+ const now = Date.now();
582
+ const resets = [rolling, weekly, monthly]
583
+ .filter((w): w is any => !!w)
584
+ .map((w) => {
585
+ const t = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : NaN;
586
+ return Number.isFinite(t) && t > now ? t : null;
587
+ })
588
+ .filter((t): t is number => t !== null);
589
+ const nearestReset = resets.length > 0 ? Math.min(...resets) : null;
590
+
591
+ const rem = (w: any) => (w ? 100 - w.percent : null);
592
+ const r = rem(rolling);
593
+ const wk = rem(weekly);
594
+ const mo = rem(monthly);
595
+ const parts: string[] = [];
596
+ if (r !== null) parts.push(`5h: ${Math.round(r)}%`);
597
+ if (wk !== null) parts.push(`W: ${Math.round(wk)}%`);
598
+ if (mo !== null) parts.push(`M: ${Math.round(mo)}%`);
599
+ let display = parts.join(" ");
600
+ if (nearestReset) {
601
+ const diff = nearestReset - now;
602
+ if (diff > 0 && diff < 30 * 24 * 60 * 60 * 1000) {
603
+ display += ` ⏱ ${formatDuration(diff)}`;
604
+ }
605
+ }
606
+ const low = (v: number | null) => v !== null && v < 20;
607
+ const mid = (v: number | null) => v !== null && v < 50;
608
+ const color = low(r) || low(wk) || low(mo) ? "err" as const
609
+ : mid(r) || mid(wk) || mid(mo) ? "warn" as const
610
+ : "ok" as const;
611
+ return { modelPrefix: "", display, color };
612
+ },
613
+ },
546
614
  ];
547
615
 
548
616
  const DEFAULT_TOKEN_CONFIG: TokenConfig = { providerPlans: {}, ttl: 60 };
@@ -557,6 +625,7 @@ const DEFAULT_DISPLAY_CONFIG: DisplayConfig = {
557
625
  context: true,
558
626
  quota5h: true,
559
627
  quotaWeek: true,
628
+ quotaMonth: true,
560
629
  quotaClock: true,
561
630
  },
562
631
  contextStyle: "pct-window",
@@ -786,6 +855,10 @@ export function createTokenStats(
786
855
  const m = fullDisplay.match(/\bW:\s+\d+%/);
787
856
  if (m) filteredParts.push(m[0]);
788
857
  }
858
+ if (cfg.quotaMonth) {
859
+ const m = fullDisplay.match(/\bM:\s+\d+%/);
860
+ if (m) filteredParts.push(m[0]);
861
+ }
789
862
  if (cfg.quotaClock) {
790
863
  const m = fullDisplay.match(/⏱\s*\d+[hm]/);
791
864
  if (m) filteredParts.push(m[0]);
@@ -1969,12 +2042,12 @@ export function createTokenStats(
1969
2042
  } else if (subChoice === "显示内容") {
1970
2043
  const itemLabels: DisplayKey[] = [
1971
2044
  "input", "output", "totalTokens", "cacheHit", "speed", "context",
1972
- "quota5h", "quotaWeek", "quotaClock",
2045
+ "quota5h", "quotaWeek", "quotaMonth", "quotaClock",
1973
2046
  ];
1974
2047
  const itemNames: Record<DisplayKey, string> = {
1975
2048
  input: "输入", output: "输出", totalTokens: "总token",
1976
2049
  cacheHit: "缓存命中", speed: "速度", context: "容量",
1977
- quota5h: "5h额度", quotaWeek: "周额度", quotaClock: "刷新时间",
2050
+ quota5h: "5h额度", quotaWeek: "周额度", quotaMonth: "月额度", quotaClock: "刷新时间",
1978
2051
  };
1979
2052
  while (true) {
1980
2053
  const options = itemLabels.map(k =>