token-stats-timer 1.0.13 → 1.0.14
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 +22 -2
- package/package.json +1 -1
- package/token-stats.ts +199 -20
package/README.md
CHANGED
|
@@ -76,11 +76,31 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
76
76
|
|
|
77
77
|
## 命令
|
|
78
78
|
|
|
79
|
-
- `/stats` —— 无参进入套餐配置(为当前 provider
|
|
79
|
+
- `/stats` —— 无参进入套餐配置(为当前 provider 选择/关闭配额套餐;选 GLM 后会继续询问是否配置团队套餐凭证)
|
|
80
80
|
- `/stats day [YYYY-MM-DD]` / `hour` / `week` / `month [YYYY-MM]` —— 统计查询
|
|
81
|
-
- `/stats config` —— 显示样式 / 显示内容 / 配额刷新时间
|
|
81
|
+
- `/stats config` —— 显示样式 / 显示内容 / 配额刷新时间 / GLM 团队凭证
|
|
82
82
|
- `/notify [on|off|test]` —— 通知开关 / 测试(无参查看状态)
|
|
83
83
|
|
|
84
|
+
## GLM 团队套餐(Team Plan)
|
|
85
|
+
|
|
86
|
+
个人版与团队版共用 `GET /api/monitor/usage/quota/limit`,区别在请求头:团队版需额外携带 `Bigmodel-Organization` / `Bigmodel-Project` 两个请求头并加 `?type=2`(api_key + 组织 ID + 项目 ID 三者缺一不可,仅国内站 `open.bigmodel.cn` 有团队档)。
|
|
87
|
+
|
|
88
|
+
本插件在**组织 ID 与项目 ID 都配置**时才走团队查询,否则回退个人版查询:
|
|
89
|
+
|
|
90
|
+
- 启用 GLM 套餐后(`/stats` 选 GLM)会自动弹出团队凭证配置询问,可「✏️ 配置/修改」或「跳过」
|
|
91
|
+
- 随时可通过 `/stats config` → 「GLM 团队凭证」修改或清除
|
|
92
|
+
- 凭证保存在 `~/.pi/agent/extensions/token-stats/config.json` 的 `teamCredential` 字段:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"providerPlans": { "zai-coding-cn": "glm" },
|
|
97
|
+
"teamCredential": { "organization": "your-org-id", "project": "your-project-id" },
|
|
98
|
+
"ttl": 60
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> 组织 ID / 项目 ID 在 GLM Coding Plan 团队版后台「团队编程套餐」页面获取;如果你同时使用 Claude Code / Cursor 等工具并已在环境变量里配置,也可以在 config.json 里直接填上同名值。
|
|
103
|
+
|
|
84
104
|
## 与原包的兼容性
|
|
85
105
|
|
|
86
106
|
- 显示/套餐配置沿用 `~/.pi/agent/extensions/token-stats/`(原 token-stats 的配置直接生效)
|
package/package.json
CHANGED
package/token-stats.ts
CHANGED
|
@@ -100,6 +100,17 @@ interface DailyRecord {
|
|
|
100
100
|
|
|
101
101
|
// ── 套餐用量类型 ──────────────────────────────────────────
|
|
102
102
|
|
|
103
|
+
/** GLM / 智谱团队套餐凭证:组织 ID + 项目 ID(二者齐全才走团队查询) */
|
|
104
|
+
interface TeamCredential {
|
|
105
|
+
organization: string;
|
|
106
|
+
project: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** fetchQuota 额外参数(目前仅 GLM 团队版使用) */
|
|
110
|
+
interface QuotaFetchExtra {
|
|
111
|
+
team?: TeamCredential | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
interface TokenPlan {
|
|
104
115
|
id: string;
|
|
105
116
|
name: string;
|
|
@@ -108,12 +119,14 @@ interface TokenPlan {
|
|
|
108
119
|
baseUrl: string;
|
|
109
120
|
quotaPath: string;
|
|
110
121
|
authHeader: (key: string) => Record<string, string>;
|
|
111
|
-
fetchQuota: (plan: TokenPlan, key: string) => Promise<any>;
|
|
122
|
+
fetchQuota: (plan: TokenPlan, key: string, extra?: QuotaFetchExtra) => Promise<any>;
|
|
112
123
|
format: (data: any) => { modelPrefix: string; display: string; color: 'ok' | 'warn' | 'err' };
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
interface TokenConfig {
|
|
116
127
|
providerPlans: Record<string, string | null>;
|
|
128
|
+
/** GLM 团队套餐凭证(可选),经 /stats 菜单配置并持久化 */
|
|
129
|
+
teamCredential?: { organization: string; project: string };
|
|
117
130
|
ttl: number;
|
|
118
131
|
}
|
|
119
132
|
|
|
@@ -334,15 +347,27 @@ const BUILTIN_PLANS: TokenPlan[] = [
|
|
|
334
347
|
{
|
|
335
348
|
id: "glm",
|
|
336
349
|
name: "GLM (智谱)",
|
|
337
|
-
matchProviders: ["zhipu-cn", "zhipu", "glm", "bigmodel"],
|
|
350
|
+
matchProviders: ["zhipu-cn", "zhipu", "glm", "bigmodel", "zai-coding-cn"],
|
|
338
351
|
apiKeyEnv: "GLM_API_KEY",
|
|
339
352
|
baseUrl: "https://open.bigmodel.cn",
|
|
340
353
|
quotaPath: "/api/monitor/usage/quota/limit",
|
|
341
354
|
authHeader: (key) => ({ Authorization: key }),
|
|
342
|
-
fetchQuota: async (plan: TokenPlan, key: string) => {
|
|
343
|
-
const
|
|
355
|
+
fetchQuota: async (plan: TokenPlan, key: string, extra?: QuotaFetchExtra) => {
|
|
356
|
+
const team = extra?.team;
|
|
357
|
+
const headers: Record<string, string> = {
|
|
358
|
+
...plan.authHeader(key),
|
|
359
|
+
"Content-Type": "application/json",
|
|
360
|
+
};
|
|
361
|
+
// 团队套餐:同一 quota 端点加 ?type=2,并携带组织/项目 ID 请求头
|
|
362
|
+
// (api_key + 组织 ID + 项目 ID 三者缺一不可,仅国内站 open.bigmodel.cn 有团队版)
|
|
363
|
+
if (team) {
|
|
364
|
+
headers["Bigmodel-Organization"] = team.organization;
|
|
365
|
+
headers["Bigmodel-Project"] = team.project;
|
|
366
|
+
}
|
|
367
|
+
const url = plan.baseUrl + plan.quotaPath + (team ? "?type=2" : "");
|
|
368
|
+
const r = await fetch(url, {
|
|
344
369
|
method: "GET",
|
|
345
|
-
headers
|
|
370
|
+
headers,
|
|
346
371
|
signal: AbortSignal.timeout(5000),
|
|
347
372
|
});
|
|
348
373
|
if (!r.ok) throw new Error("GLM 配额查询 HTTP " + r.status);
|
|
@@ -350,23 +375,56 @@ const BUILTIN_PLANS: TokenPlan[] = [
|
|
|
350
375
|
},
|
|
351
376
|
format: (data: any) => {
|
|
352
377
|
const limits = data?.data?.limits || [];
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
378
|
+
// 个人版返回 TOKENS_LIMIT,团队版返回 CREDIT_LIMIT(大小写不敏感)必识
|
|
379
|
+
const isQuota = (t: any) => {
|
|
380
|
+
const s = String(t ?? "").toLowerCase();
|
|
381
|
+
return s === "tokens_limit" || s === "credit_limit";
|
|
382
|
+
};
|
|
383
|
+
const entries = limits.filter((x: any) => isQuota(x?.type));
|
|
384
|
+
if (entries.length === 0) return { modelPrefix: "", display: "无数据", color: "err" as const };
|
|
385
|
+
|
|
386
|
+
// 窗口分类锚定 unit(不依赖数组顺序,与 cc-switch parse_zhipu_token_tiers 一致):
|
|
387
|
+
// unit: 3 → 5h 滚动窗口;unit: 6 → 每周窗口(老/新套餐 number 7 / 1 均可能)
|
|
388
|
+
const byUnit = (u: number) => entries.find((x: any) => x?.unit === u);
|
|
389
|
+
let fiveHour: any = byUnit(3) ?? null;
|
|
390
|
+
let weekly: any = byUnit(6) ?? null;
|
|
391
|
+
// 兜底:unit 缺失/不识别 → 无 nextResetTime 优先归 5h,其余按重置时间升序补槽
|
|
392
|
+
if (!fiveHour || !weekly) {
|
|
393
|
+
const unclassified = entries
|
|
394
|
+
.filter((x: any) => x !== fiveHour && x !== weekly)
|
|
395
|
+
.sort((a: any, b: any) =>
|
|
396
|
+
(typeof a?.nextResetTime === "number" ? a.nextResetTime : Number.MIN_SAFE_INTEGER) -
|
|
397
|
+
(typeof b?.nextResetTime === "number" ? b.nextResetTime : Number.MIN_SAFE_INTEGER));
|
|
398
|
+
for (const e of unclassified) {
|
|
399
|
+
if (!fiveHour) fiveHour = e;
|
|
400
|
+
else if (!weekly) weekly = e;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// percentage 为已用比例 → 剩余 = 100 - 已用
|
|
405
|
+
const intervalRemaining = fiveHour ? 100 - (fiveHour.percentage ?? 0) : null;
|
|
406
|
+
const weeklyRemaining = weekly ? 100 - (weekly.percentage ?? 0) : null;
|
|
360
407
|
const now = Date.now();
|
|
361
|
-
const resets =
|
|
362
|
-
.map((x: any) => x
|
|
408
|
+
const resets = entries
|
|
409
|
+
.map((x: any) => x?.nextResetTime)
|
|
363
410
|
.filter((t: any) => typeof t === "number" && t > now);
|
|
364
411
|
const nearestReset = resets.length > 0 ? Math.min(...resets) : null;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
412
|
+
|
|
413
|
+
const both = intervalRemaining !== null && weeklyRemaining !== null;
|
|
414
|
+
const display =
|
|
415
|
+
intervalRemaining !== null && weeklyRemaining !== null
|
|
416
|
+
? formatTokenPlanDisplay(intervalRemaining, weeklyRemaining, nearestReset)
|
|
417
|
+
: intervalRemaining !== null
|
|
418
|
+
? `5h: ${Math.round(intervalRemaining)}%`
|
|
419
|
+
: weeklyRemaining !== null
|
|
420
|
+
? `W: ${Math.round(weeklyRemaining)}%`
|
|
421
|
+
: "无数据";
|
|
422
|
+
const low = (v: number | null) => v !== null && v < 20;
|
|
423
|
+
const mid = (v: number | null) => v !== null && v < 50;
|
|
424
|
+
const color = low(intervalRemaining) || low(weeklyRemaining) ? "err" as const
|
|
425
|
+
: mid(intervalRemaining) || mid(weeklyRemaining) ? "warn" as const
|
|
426
|
+
: "ok" as const;
|
|
427
|
+
return { modelPrefix: "", display, color };
|
|
370
428
|
},
|
|
371
429
|
},
|
|
372
430
|
{
|
|
@@ -1017,6 +1075,20 @@ export function createTokenStats(
|
|
|
1017
1075
|
return null;
|
|
1018
1076
|
}
|
|
1019
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* 解析套餐的团队凭证(当前仅 GLM/智谱团队版)。
|
|
1080
|
+
* 组织 ID + 项目 ID 二者齐全才返回,否则返回 null(回退个人版查询)。
|
|
1081
|
+
* 来源:token-stats config.json 的 teamCredential(/stats 菜单配置)。
|
|
1082
|
+
*/
|
|
1083
|
+
function resolveTeamCredential(plan: TokenPlan): TeamCredential | null {
|
|
1084
|
+
if (plan.id !== "glm") return null;
|
|
1085
|
+
const tc = tokenConfig?.teamCredential;
|
|
1086
|
+
const organization = tc?.organization?.trim() ?? "";
|
|
1087
|
+
const project = tc?.project?.trim() ?? "";
|
|
1088
|
+
if (organization && project) return { organization, project };
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1020
1092
|
/**
|
|
1021
1093
|
* 检测并处理 provider 变化。
|
|
1022
1094
|
* 返回 true 表示发生了切换(供调用者决定是否要 force refresh)。
|
|
@@ -1138,7 +1210,11 @@ export function createTokenStats(
|
|
|
1138
1210
|
|
|
1139
1211
|
// 5. 调接口
|
|
1140
1212
|
try {
|
|
1141
|
-
const data = await plan.fetchQuota(
|
|
1213
|
+
const data = await plan.fetchQuota(
|
|
1214
|
+
plan,
|
|
1215
|
+
key,
|
|
1216
|
+
{ team: resolveTeamCredential(plan) },
|
|
1217
|
+
);
|
|
1142
1218
|
cache[plan.id] = { fetchedAt: Date.now(), ttl: ttlMs, data };
|
|
1143
1219
|
await writeQuotaCache(cache);
|
|
1144
1220
|
const fmt = plan.format(data);
|
|
@@ -1173,6 +1249,58 @@ export function createTokenStats(
|
|
|
1173
1249
|
shared.requestRender?.();
|
|
1174
1250
|
}
|
|
1175
1251
|
|
|
1252
|
+
/** config.json 无参流里的基础结构(避免 null 展开报错) */
|
|
1253
|
+
function baseTokenConfig(): TokenConfig {
|
|
1254
|
+
return { ...(tokenConfig ?? { providerPlans: {}, ttl: 60 }) };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* GLM 团队套餐交互配置:提示用户填写组织 ID / 项目 ID 并持久化到 config.json,
|
|
1259
|
+
* 保存后 force 刷新使团队查询(?type=2)立即生效,并反馈查询结果。
|
|
1260
|
+
*/
|
|
1261
|
+
async function promptGlmTeamConfig(ctx: ExtensionContext): Promise<void> {
|
|
1262
|
+
const cur = tokenConfig?.teamCredential;
|
|
1263
|
+
const curLabel =
|
|
1264
|
+
cur?.organization && cur?.project
|
|
1265
|
+
? `${cur.organization} / ${cur.project}`
|
|
1266
|
+
: "未配置(按个人版查询)";
|
|
1267
|
+
const choice = await ctx.ui.select(
|
|
1268
|
+
`GLM 团队套餐凭证?当前:${curLabel}\n填写组织 ID + 项目 ID(二者齐全才走团队查询 ?type=2),跳过则维持个人版查询`,
|
|
1269
|
+
["✏️ 配置/修改", "跳过"],
|
|
1270
|
+
);
|
|
1271
|
+
if (!choice || choice === "跳过") {
|
|
1272
|
+
await forceRefreshQuota(ctx);
|
|
1273
|
+
const errMsg = quotaState?.error ? formatQuotaError(quotaState) : "";
|
|
1274
|
+
ctx.ui.notify(
|
|
1275
|
+
quotaState?.error ? `GLM 配额查询失败:${errMsg}` : "GLM 配额已启用(个人版查询)",
|
|
1276
|
+
"info",
|
|
1277
|
+
);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const organization = await ctx.ui.input("组织 ID (Organization)", cur?.organization ?? "");
|
|
1282
|
+
const project = await ctx.ui.input("项目 ID (Project)", cur?.project ?? "");
|
|
1283
|
+
const org = organization?.trim() ?? "";
|
|
1284
|
+
const proj = project?.trim() ?? "";
|
|
1285
|
+
if (!org || !proj) {
|
|
1286
|
+
ctx.ui.notify("组织/项目 ID 不能为空,团队凭证未保存", "warning");
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
tokenConfig = {
|
|
1291
|
+
...baseTokenConfig(),
|
|
1292
|
+
teamCredential: { organization: org, project: proj },
|
|
1293
|
+
};
|
|
1294
|
+
await saveTokenConfig(tokenConfig);
|
|
1295
|
+
|
|
1296
|
+
await forceRefreshQuota(ctx);
|
|
1297
|
+
const errMsg = quotaState?.error ? formatQuotaError(quotaState) : "";
|
|
1298
|
+
ctx.ui.notify(
|
|
1299
|
+
quotaState?.error ? `GLM 团队配额查询失败:${errMsg}` : "GLM 团队套餐配额已启用",
|
|
1300
|
+
"info",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1176
1304
|
/** 清空所有套餐缓存(session_start 调,避免跨 session 复用旧数据) */
|
|
1177
1305
|
async function invalidateAllQuotaCache() {
|
|
1178
1306
|
try {
|
|
@@ -1668,6 +1796,11 @@ export function createTokenStats(
|
|
|
1668
1796
|
} catch { /* ctx 已失效(session 被替换),忽略 */ }
|
|
1669
1797
|
shared.requestRender?.();
|
|
1670
1798
|
}, (tokenConfig?.ttl || 60) * 1000);
|
|
1799
|
+
if (plan.id === "glm") {
|
|
1800
|
+
// GLM:继续询问团队套餐凭证(个人版/团队版二选一,内部会 force 查询并反馈)
|
|
1801
|
+
await promptGlmTeamConfig(ctx);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1671
1804
|
if (quotaState?.error) {
|
|
1672
1805
|
// 仅当 quotaState 带有 error 字段时(key 缺失 / API 错误 / 网络错误 / 无数据)才提示"查询失败"
|
|
1673
1806
|
const errMsg = formatQuotaError(quotaState);
|
|
@@ -1684,9 +1817,55 @@ export function createTokenStats(
|
|
|
1684
1817
|
"显示样式",
|
|
1685
1818
|
"显示内容",
|
|
1686
1819
|
"刷新时间 (当前 " + (tokenConfig?.ttl || 60) + "s)",
|
|
1820
|
+
"GLM 团队凭证",
|
|
1687
1821
|
]);
|
|
1688
1822
|
if (!subChoice) return;
|
|
1689
1823
|
|
|
1824
|
+
if (subChoice === "GLM 团队凭证") {
|
|
1825
|
+
const cur = tokenConfig?.teamCredential;
|
|
1826
|
+
const label =
|
|
1827
|
+
cur?.organization && cur?.project
|
|
1828
|
+
? `${cur.organization} / ${cur.project}`
|
|
1829
|
+
: "未配置";
|
|
1830
|
+
const action = await ctx.ui.select("GLM 团队凭证(当前: " + label + ")", [
|
|
1831
|
+
"✏️ 配置/修改",
|
|
1832
|
+
"清除",
|
|
1833
|
+
"返回",
|
|
1834
|
+
]);
|
|
1835
|
+
if (!action || action === "返回") return;
|
|
1836
|
+
if (action === "清除") {
|
|
1837
|
+
tokenConfig = {
|
|
1838
|
+
...baseTokenConfig(),
|
|
1839
|
+
teamCredential: { organization: "", project: "" },
|
|
1840
|
+
};
|
|
1841
|
+
await saveTokenConfig(tokenConfig);
|
|
1842
|
+
await forceRefreshQuota(ctx);
|
|
1843
|
+
ctx.ui.notify("GLM 团队凭证已清除(恢复个人版查询)", "info");
|
|
1844
|
+
} else {
|
|
1845
|
+
const organization = await ctx.ui.input("组织 ID (Organization)", cur?.organization ?? "");
|
|
1846
|
+
const project = await ctx.ui.input("项目 ID (Project)", cur?.project ?? "");
|
|
1847
|
+
const org = organization?.trim() ?? "";
|
|
1848
|
+
const proj = project?.trim() ?? "";
|
|
1849
|
+
if (!org || !proj) {
|
|
1850
|
+
ctx.ui.notify("组织/项目 ID 不能为空,未保存", "warning");
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
tokenConfig = {
|
|
1854
|
+
...baseTokenConfig(),
|
|
1855
|
+
teamCredential: { organization: org, project: proj },
|
|
1856
|
+
};
|
|
1857
|
+
await saveTokenConfig(tokenConfig);
|
|
1858
|
+
await forceRefreshQuota(ctx);
|
|
1859
|
+
const errMsg = quotaState?.error ? formatQuotaError(quotaState) : "";
|
|
1860
|
+
if (quotaState?.error) {
|
|
1861
|
+
ctx.ui.notify(`GLM 团队配额查询失败:${errMsg}`, "info");
|
|
1862
|
+
} else {
|
|
1863
|
+
ctx.ui.notify("GLM 团队凭证已保存(团队查询已生效)", "info");
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1690
1869
|
if (subChoice === "显示样式") {
|
|
1691
1870
|
const catChoice = await ctx.ui.select("选择要配置的样式类别", [
|
|
1692
1871
|
"上下文样式",
|