glm-quota-line 0.4.0 → 0.6.0
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/CHANGELOG.md +12 -0
- package/README.md +12 -7
- package/README.zh-CN.md +13 -8
- package/package.json +1 -1
- package/src/core/quota/cache.js +55 -7
- package/src/core/quota/parse.js +141 -46
- package/src/core/status/format.js +92 -39
- package/src/core/status/viewModel.js +73 -52
- package/src/shared/config.js +12 -5
- package/src/shared/constants.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- Added official international GLM quota endpoint support for `api.z.ai` while preserving domestic `open.bigmodel.cn` detection
|
|
6
|
+
- Added dual-token status rendering for the new package shape, showing both the 5h quota and weekly quota in `text`, `compact`, and `bar` styles
|
|
7
|
+
- Kept old packages compatible by continuing to read the legacy `TOKENS_LIMIT(number=5)` quota and ignoring `TIME_LIMIT` / MCP usage
|
|
8
|
+
- Updated cache compatibility so existing cached percent-based results still render after upgrading
|
|
9
|
+
- Refreshed English and Chinese README examples to document international setup and the new token-only display behavior
|
|
10
|
+
|
|
11
|
+
## 0.5.0
|
|
12
|
+
|
|
13
|
+
- Changed bar characters from `■□` (discrete squares) to `█░` (continuous blocks) to match Claude Code official statusline style
|
|
14
|
+
|
|
3
15
|
## 0.4.0
|
|
4
16
|
|
|
5
17
|
- Added managed Claude Code `SessionStart` hooks to pre-refresh quota on `startup`, `resume`, `clear`, and `compact`
|
package/README.md
CHANGED
|
@@ -73,18 +73,20 @@ glm-quota-line --style compact
|
|
|
73
73
|
## Output
|
|
74
74
|
|
|
75
75
|
```text
|
|
76
|
-
GLM Lite | 5h left 91% | reset 14:47
|
|
77
|
-
|
|
78
|
-
GLM
|
|
76
|
+
GLM Lite | 5h left 91% | week 47% | reset 14:47
|
|
77
|
+
|
|
78
|
+
GLM 5h 91% W 47% | 14:47
|
|
79
|
+
|
|
80
|
+
GLM Lite █░░░░░░░░░ 91% | W 47% | 14:47
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
## Recommended Setups
|
|
82
84
|
|
|
83
|
-
| Use case
|
|
84
|
-
|
|
|
85
|
-
| Dark terminal
|
|
85
|
+
| Use case | Config |
|
|
86
|
+
| -------------- | --------------------------------------------- |
|
|
87
|
+
| Dark terminal | `style=bar`, `theme=ansi`, `palette=dark` |
|
|
86
88
|
| Light terminal | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
87
|
-
| Plain fallback | any `style`, `theme=plain`
|
|
89
|
+
| Plain fallback | any `style`, `theme=plain` |
|
|
88
90
|
|
|
89
91
|
## Color Presets
|
|
90
92
|
|
|
@@ -117,6 +119,8 @@ If Claude Code is running behind a gateway or proxy and the injected `ANTHROPIC_
|
|
|
117
119
|
```bash
|
|
118
120
|
glm-quota-line config set auth-token <your-real-token>
|
|
119
121
|
glm-quota-line config set base-url https://open.bigmodel.cn/api/anthropic
|
|
122
|
+
# or
|
|
123
|
+
glm-quota-line config set base-url https://api.z.ai/api/anthropic
|
|
120
124
|
```
|
|
121
125
|
|
|
122
126
|
Clear them later with:
|
|
@@ -153,6 +157,7 @@ Run `glm-quota-line --help` for the full command descriptions and examples.
|
|
|
153
157
|
- Default output is plain text
|
|
154
158
|
- Missing or expired auth returns `GLM | auth expired`
|
|
155
159
|
- API or parsing failures return `GLM | quota unavailable`
|
|
160
|
+
- Only `TOKENS_LIMIT` quotas are shown; `TIME_LIMIT` / MCP usage is ignored
|
|
156
161
|
- `install` wires both the status line command and managed `SessionStart` hooks
|
|
157
162
|
- `install` does not replace an unmanaged Claude status line unless `--force` is used
|
|
158
163
|
- `install --force` backs up the previous entry and `uninstall` restores it when possible
|
package/README.zh-CN.md
CHANGED
|
@@ -73,18 +73,20 @@ glm-quota-line --style compact
|
|
|
73
73
|
## 输出示例
|
|
74
74
|
|
|
75
75
|
```text
|
|
76
|
-
GLM Lite | 5h
|
|
77
|
-
|
|
78
|
-
GLM
|
|
76
|
+
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
77
|
+
|
|
78
|
+
GLM 5h 91% W 47% | 14:47
|
|
79
|
+
|
|
80
|
+
GLM Lite █░░░░░░░░░ 91% | W 47% | 14:47
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
## 推荐搭配
|
|
82
84
|
|
|
83
|
-
| 使用场景
|
|
84
|
-
|
|
|
85
|
-
| 深色终端
|
|
86
|
-
| 浅色终端
|
|
87
|
-
| 纯文本回退 | 任意 `style`,`theme=plain`
|
|
85
|
+
| 使用场景 | 配置 |
|
|
86
|
+
| ---------- | --------------------------------------------- |
|
|
87
|
+
| 深色终端 | `style=bar`, `theme=ansi`, `palette=dark` |
|
|
88
|
+
| 浅色终端 | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
89
|
+
| 纯文本回退 | 任意 `style`,`theme=plain` |
|
|
88
90
|
|
|
89
91
|
## 颜色搭配
|
|
90
92
|
|
|
@@ -117,6 +119,8 @@ glm-quota-line config set theme plain
|
|
|
117
119
|
```bash
|
|
118
120
|
glm-quota-line config set auth-token <your-real-token>
|
|
119
121
|
glm-quota-line config set base-url https://open.bigmodel.cn/api/anthropic
|
|
122
|
+
# 或
|
|
123
|
+
glm-quota-line config set base-url https://api.z.ai/api/anthropic
|
|
120
124
|
```
|
|
121
125
|
|
|
122
126
|
清除覆盖值:
|
|
@@ -157,6 +161,7 @@ glm-quota-line --help
|
|
|
157
161
|
- 默认输出为纯文本
|
|
158
162
|
- 鉴权缺失或失效时返回 `GLM | auth expired`
|
|
159
163
|
- 接口异常或解析失败时返回 `GLM | quota unavailable`
|
|
164
|
+
- 只展示 `TOKENS_LIMIT` 配额,忽略 `TIME_LIMIT` / MCP 使用量
|
|
160
165
|
- `install` 会同时接入状态栏命令和受工具管理的 `SessionStart` hooks
|
|
161
166
|
- `install` 默认不会覆盖非本工具管理的 Claude 状态栏,除非显式使用 `--force`
|
|
162
167
|
- `install --force` 会备份旧配置,`uninstall` 会在可能时恢复
|
package/package.json
CHANGED
package/src/core/quota/cache.js
CHANGED
|
@@ -1,24 +1,72 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
function
|
|
4
|
+
function isValidQuotaShape(value) {
|
|
5
|
+
if (!value || typeof value.key !== "string") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!Number.isFinite(value.leftPercent)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!Number.isFinite(value.usedPercent)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if ("nextResetTime" in value && !Number.isFinite(value.nextResetTime)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isLegacyPercentSuccessCacheShape(value) {
|
|
25
|
+
if (!value || value.kind !== "success" || typeof value.level !== "string") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value.display !== "percent") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!Number.isFinite(value.leftPercent)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ("usedPercent" in value && !Number.isFinite(value.usedPercent)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if ("nextResetTime" in value && !Number.isFinite(value.nextResetTime)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isMultiQuotaSuccessCacheShape(value) {
|
|
5
49
|
if (!value || value.kind !== "success" || typeof value.level !== "string") {
|
|
6
50
|
return false;
|
|
7
51
|
}
|
|
8
52
|
|
|
9
|
-
if (!
|
|
53
|
+
if (!Array.isArray(value.quotas) || value.quotas.length === 0) {
|
|
10
54
|
return false;
|
|
11
55
|
}
|
|
12
56
|
|
|
13
|
-
if (value.
|
|
14
|
-
return
|
|
57
|
+
if (!value.quotas.every(isValidQuotaShape)) {
|
|
58
|
+
return false;
|
|
15
59
|
}
|
|
16
60
|
|
|
17
|
-
if (value.
|
|
18
|
-
return
|
|
61
|
+
if ("primaryQuotaKey" in value && typeof value.primaryQuotaKey !== "string") {
|
|
62
|
+
return false;
|
|
19
63
|
}
|
|
20
64
|
|
|
21
|
-
return
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSuccessCacheShape(value) {
|
|
69
|
+
return isMultiQuotaSuccessCacheShape(value) || isLegacyPercentSuccessCacheShape(value);
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
function asFiniteNumber(value) {
|
package/src/core/quota/parse.js
CHANGED
|
@@ -18,28 +18,145 @@ function isRateLimitedMessage(value) {
|
|
|
18
18
|
return /rate\s*limit|too many requests|too frequent|frequency|\u9650\u6d41|\u9891\u7387|\u8fc7\u4e8e\u9891\u7e41|\u7a0d\u540e\u518d\u8bd5/i.test(value);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
21
|
+
function clampPercent(value) {
|
|
22
|
+
const percent = asFiniteNumber(value);
|
|
23
|
+
if (percent === null) {
|
|
24
|
+
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
return Math.min(100, Math.max(0, percent));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function computePercentages(limit) {
|
|
31
|
+
const usage = asFiniteNumber(limit?.usage);
|
|
32
|
+
const remaining = asFiniteNumber(limit?.remaining);
|
|
33
|
+
const currentValue = asFiniteNumber(limit?.currentValue);
|
|
34
|
+
const totalFromParts =
|
|
35
|
+
remaining !== null && currentValue !== null ? remaining + currentValue : null;
|
|
36
|
+
const total = totalFromParts !== null && totalFromParts > 0 ? totalFromParts : usage;
|
|
37
|
+
|
|
38
|
+
if (total !== null && total > 0) {
|
|
39
|
+
if (remaining !== null && remaining >= 0 && remaining <= total) {
|
|
40
|
+
const leftPercent = clampPercent(Math.round((remaining / total) * 100));
|
|
41
|
+
if (leftPercent !== null) {
|
|
42
|
+
return {
|
|
43
|
+
leftPercent,
|
|
44
|
+
usedPercent: 100 - leftPercent
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (currentValue !== null && currentValue >= 0 && currentValue <= total) {
|
|
50
|
+
const usedPercent = clampPercent(Math.round((currentValue / total) * 100));
|
|
51
|
+
if (usedPercent !== null) {
|
|
52
|
+
return {
|
|
53
|
+
leftPercent: 100 - usedPercent,
|
|
54
|
+
usedPercent
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
31
58
|
}
|
|
32
59
|
|
|
33
|
-
|
|
60
|
+
// Legacy token payloads only expose `percentage`; historically this has
|
|
61
|
+
// represented used percent for GLM quota responses.
|
|
62
|
+
const usedPercent = clampPercent(limit?.percentage);
|
|
63
|
+
if (usedPercent === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
leftPercent: 100 - usedPercent,
|
|
69
|
+
usedPercent
|
|
70
|
+
};
|
|
34
71
|
}
|
|
35
72
|
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
73
|
+
function normalizeTokenQuota(key, limit) {
|
|
74
|
+
const percentages = computePercentages(limit);
|
|
75
|
+
if (!percentages) {
|
|
39
76
|
return null;
|
|
40
77
|
}
|
|
41
78
|
|
|
42
|
-
|
|
79
|
+
const nextResetTime = asFiniteNumber(limit?.nextResetTime);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
key,
|
|
83
|
+
leftPercent: percentages.leftPercent,
|
|
84
|
+
usedPercent: percentages.usedPercent,
|
|
85
|
+
...(nextResetTime !== null ? { nextResetTime } : {})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sortTokenLimitsByResetTime(limits) {
|
|
90
|
+
return [...limits].sort((left, right) => {
|
|
91
|
+
const leftReset = asFiniteNumber(left?.nextResetTime);
|
|
92
|
+
const rightReset = asFiniteNumber(right?.nextResetTime);
|
|
93
|
+
|
|
94
|
+
if (leftReset !== null && rightReset !== null && leftReset !== rightReset) {
|
|
95
|
+
return leftReset - rightReset;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (leftReset !== null && rightReset === null) {
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (leftReset === null && rightReset !== null) {
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (left?._index ?? 0) - (right?._index ?? 0);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pickSecondaryTokenLimit(tokenLimits, primaryLimit) {
|
|
111
|
+
const candidates = tokenLimits.filter((limit) => limit?._index !== primaryLimit?._index);
|
|
112
|
+
if (candidates.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return sortTokenLimitsByResetTime(candidates)[0] ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pickPrimaryAndSecondaryTokenLimits(limits) {
|
|
120
|
+
const tokenLimits = limits
|
|
121
|
+
.filter((limit) => limit?.type === "TOKENS_LIMIT")
|
|
122
|
+
.map((limit, index) => ({ ...limit, _index: index }));
|
|
123
|
+
|
|
124
|
+
if (tokenLimits.length === 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const explicitFiveHour = tokenLimits.find((limit) => limit?.number === 5) ?? null;
|
|
129
|
+
if (explicitFiveHour) {
|
|
130
|
+
const weekCandidate = pickSecondaryTokenLimit(tokenLimits, explicitFiveHour);
|
|
131
|
+
return [explicitFiveHour, weekCandidate].filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (tokenLimits.length === 1) {
|
|
135
|
+
return tokenLimits;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return sortTokenLimitsByResetTime(tokenLimits).slice(0, 2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildTokenQuotas(limits) {
|
|
142
|
+
const selectedLimits = pickPrimaryAndSecondaryTokenLimits(limits);
|
|
143
|
+
if (selectedLimits.length === 0) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const quotas = [];
|
|
148
|
+
const [fiveHourLimit, weekLimit] = selectedLimits;
|
|
149
|
+
const fiveHourQuota = normalizeTokenQuota("token_5h", fiveHourLimit);
|
|
150
|
+
if (fiveHourQuota) {
|
|
151
|
+
quotas.push(fiveHourQuota);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const weekQuota = normalizeTokenQuota("token_week", weekLimit);
|
|
155
|
+
if (weekQuota) {
|
|
156
|
+
quotas.push(weekQuota);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return quotas;
|
|
43
160
|
}
|
|
44
161
|
|
|
45
162
|
export function parseQuotaResponse(response) {
|
|
@@ -70,45 +187,23 @@ export function parseQuotaResponse(response) {
|
|
|
70
187
|
|
|
71
188
|
const level = typeof payload.data?.level === "string" ? payload.data.level : "";
|
|
72
189
|
const limits = Array.isArray(payload.data?.limits) ? payload.data.limits : [];
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (rollingWindowLimit) {
|
|
78
|
-
const usedPercent = clampPercent(rollingWindowLimit.percentage);
|
|
79
|
-
const nextResetTime = asFiniteNumber(rollingWindowLimit.nextResetTime);
|
|
80
|
-
|
|
81
|
-
if (usedPercent !== null && nextResetTime !== null) {
|
|
82
|
-
return {
|
|
83
|
-
kind: "success",
|
|
84
|
-
level,
|
|
85
|
-
display: "percent",
|
|
86
|
-
leftPercent: 100 - usedPercent,
|
|
87
|
-
usedPercent,
|
|
88
|
-
nextResetTime
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const timeLimit = limits.find((limit) => limit?.type === "TIME_LIMIT" && limit?.unit === 5);
|
|
94
|
-
if (!timeLimit) {
|
|
190
|
+
const quotas = buildTokenQuotas(limits);
|
|
191
|
+
if (quotas.length === 0) {
|
|
95
192
|
return { kind: "unavailable" };
|
|
96
193
|
}
|
|
97
194
|
|
|
98
|
-
const
|
|
99
|
-
const total = computeTotal(timeLimit);
|
|
100
|
-
const nextResetTime = asFiniteNumber(timeLimit.nextResetTime);
|
|
101
|
-
|
|
102
|
-
if (remaining === null || total === null || nextResetTime === null) {
|
|
103
|
-
return { kind: "unavailable" };
|
|
104
|
-
}
|
|
195
|
+
const primaryQuota = quotas[0];
|
|
105
196
|
|
|
106
197
|
return {
|
|
107
198
|
kind: "success",
|
|
108
199
|
level,
|
|
109
|
-
display: "
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
nextResetTime
|
|
200
|
+
display: "percent",
|
|
201
|
+
leftPercent: primaryQuota.leftPercent,
|
|
202
|
+
usedPercent: primaryQuota.usedPercent,
|
|
203
|
+
...(Number.isFinite(primaryQuota.nextResetTime)
|
|
204
|
+
? { nextResetTime: primaryQuota.nextResetTime }
|
|
205
|
+
: {}),
|
|
206
|
+
primaryQuotaKey: primaryQuota.key,
|
|
207
|
+
quotas
|
|
113
208
|
};
|
|
114
209
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AUTH_EXPIRED_TEXT,
|
|
3
|
-
QUOTA_UNAVAILABLE_TEXT,
|
|
4
2
|
normalizeDisplayMode,
|
|
5
3
|
normalizePalette,
|
|
6
4
|
normalizeStatusStyle,
|
|
@@ -26,76 +24,131 @@ function createErrorSegments(model) {
|
|
|
26
24
|
];
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function
|
|
27
|
+
function createQuotaTextSegments(quota, displayMode, tone) {
|
|
30
28
|
const mode = normalizeDisplayMode(displayMode);
|
|
31
|
-
const severityTone = getSeverityTone(model);
|
|
32
29
|
|
|
33
|
-
if (mode === "used"
|
|
30
|
+
if (mode === "used") {
|
|
34
31
|
return [
|
|
35
|
-
{ text:
|
|
36
|
-
{ text:
|
|
37
|
-
{ text: model.usedText, tone: severityTone },
|
|
38
|
-
{ text: " | reset ", tone: "muted" },
|
|
39
|
-
{ text: model.resetText, tone: "reset" }
|
|
32
|
+
{ text: `${quota.label} used `, tone: "muted" },
|
|
33
|
+
{ text: quota.usedText, tone }
|
|
40
34
|
];
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
if (mode === "both"
|
|
37
|
+
if (mode === "both") {
|
|
44
38
|
return [
|
|
45
|
-
{ text:
|
|
46
|
-
{ text:
|
|
47
|
-
{ text: model.leftText, tone: severityTone },
|
|
39
|
+
{ text: `${quota.label} left `, tone: "muted" },
|
|
40
|
+
{ text: quota.leftText, tone },
|
|
48
41
|
{ text: " used ", tone: "muted" },
|
|
49
|
-
{ text:
|
|
50
|
-
{ text: " | reset ", tone: "muted" },
|
|
51
|
-
{ text: model.resetText, tone: "reset" }
|
|
42
|
+
{ text: quota.usedText, tone }
|
|
52
43
|
];
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
return [
|
|
47
|
+
{ text: `${quota.label} `, tone: "muted" },
|
|
48
|
+
{ text: quota.leftText, tone }
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function appendSecondarySegments(segments, model) {
|
|
53
|
+
if (!model.secondaryQuota) {
|
|
54
|
+
return segments;
|
|
63
55
|
}
|
|
64
56
|
|
|
65
|
-
return
|
|
57
|
+
return [
|
|
58
|
+
...segments,
|
|
59
|
+
{ text: " | ", tone: "muted" },
|
|
60
|
+
{ text: `${model.secondaryQuota.label} `, tone: "muted" },
|
|
61
|
+
{ text: model.secondaryQuota.leftText, tone: "plain" }
|
|
62
|
+
];
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
function
|
|
69
|
-
if (!
|
|
70
|
-
return
|
|
65
|
+
function appendResetSegments(segments, model) {
|
|
66
|
+
if (!model.resetText) {
|
|
67
|
+
return segments;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
return [
|
|
74
|
-
|
|
75
|
-
{ text:
|
|
76
|
-
{ text: " | ", tone: "muted" },
|
|
71
|
+
...segments,
|
|
72
|
+
{ text: " | reset ", tone: "muted" },
|
|
77
73
|
{ text: model.resetText, tone: "reset" }
|
|
78
74
|
];
|
|
79
75
|
}
|
|
80
76
|
|
|
77
|
+
function createTextSegments(model, displayMode) {
|
|
78
|
+
const severityTone = getSeverityTone(model);
|
|
79
|
+
|
|
80
|
+
return appendResetSegments(
|
|
81
|
+
appendSecondarySegments(
|
|
82
|
+
[
|
|
83
|
+
{ text: model.levelLabel, tone: "label" },
|
|
84
|
+
{ text: " | ", tone: "muted" },
|
|
85
|
+
...createQuotaTextSegments(model.primaryQuota, displayMode, severityTone)
|
|
86
|
+
],
|
|
87
|
+
model
|
|
88
|
+
),
|
|
89
|
+
model
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createCompactSegments(model) {
|
|
94
|
+
const severityTone = getSeverityTone(model);
|
|
95
|
+
let segments;
|
|
96
|
+
|
|
97
|
+
if (model.secondaryQuota) {
|
|
98
|
+
segments = [
|
|
99
|
+
{ text: `${model.compactLabel} `, tone: "label" },
|
|
100
|
+
{ text: `${model.primaryQuota.compactLabel} `, tone: "muted" },
|
|
101
|
+
{ text: model.primaryQuota.leftText, tone: severityTone },
|
|
102
|
+
{ text: " ", tone: "plain" },
|
|
103
|
+
{ text: `${model.secondaryQuota.compactLabel} `, tone: "muted" },
|
|
104
|
+
{ text: model.secondaryQuota.leftText, tone: "plain" }
|
|
105
|
+
];
|
|
106
|
+
} else {
|
|
107
|
+
segments = [
|
|
108
|
+
{ text: `${model.compactLabel} `, tone: "label" },
|
|
109
|
+
{ text: model.primaryQuota.leftText, tone: severityTone }
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (model.resetText) {
|
|
114
|
+
segments.push({ text: " | ", tone: "muted" }, { text: model.resetText, tone: "reset" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return segments;
|
|
118
|
+
}
|
|
119
|
+
|
|
81
120
|
function createBarSegments(model, barWidth) {
|
|
82
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
!Number.isFinite(model.primaryQuota?.leftPercent) ||
|
|
123
|
+
!Number.isFinite(model.primaryQuota?.usedPercent)
|
|
124
|
+
) {
|
|
83
125
|
return createErrorSegments({ kind: "unavailable" });
|
|
84
126
|
}
|
|
85
127
|
|
|
86
|
-
const bar = buildBar(model.usedPercent, barWidth);
|
|
128
|
+
const bar = buildBar(model.primaryQuota.usedPercent, barWidth);
|
|
87
129
|
const severityTone = getSeverityTone(model);
|
|
88
|
-
|
|
89
|
-
return [
|
|
130
|
+
const segments = [
|
|
90
131
|
{ text: model.levelLabel, tone: "label" },
|
|
91
132
|
{ text: " ", tone: "plain" },
|
|
92
133
|
{ text: bar.filledText, tone: severityTone },
|
|
93
134
|
{ text: bar.emptyText, tone: "barEmpty" },
|
|
94
135
|
{ text: " ", tone: "plain" },
|
|
95
|
-
{ text:
|
|
96
|
-
{ text: " | ", tone: "muted" },
|
|
97
|
-
{ text: model.resetText, tone: "reset" }
|
|
136
|
+
{ text: model.primaryQuota.leftText, tone: severityTone }
|
|
98
137
|
];
|
|
138
|
+
|
|
139
|
+
if (model.secondaryQuota) {
|
|
140
|
+
segments.push(
|
|
141
|
+
{ text: " | ", tone: "muted" },
|
|
142
|
+
{ text: `${model.secondaryQuota.compactLabel} `, tone: "muted" },
|
|
143
|
+
{ text: model.secondaryQuota.leftText, tone: "plain" }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (model.resetText) {
|
|
148
|
+
segments.push({ text: " | ", tone: "muted" }, { text: model.resetText, tone: "reset" });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return segments;
|
|
99
152
|
}
|
|
100
153
|
|
|
101
154
|
export function formatStatus(result, options = {}) {
|
|
@@ -2,6 +2,11 @@ function padTwoDigits(value) {
|
|
|
2
2
|
return String(value).padStart(2, "0");
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
const QUOTA_LABELS = {
|
|
6
|
+
token_5h: { text: "5h", compact: "5h" },
|
|
7
|
+
token_week: { text: "week", compact: "W" }
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
function formatLevel(level) {
|
|
6
11
|
if (!level) {
|
|
7
12
|
return "GLM";
|
|
@@ -11,6 +16,10 @@ function formatLevel(level) {
|
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
function formatResetTime(timestampMs) {
|
|
19
|
+
if (!Number.isFinite(timestampMs)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
const date = new Date(timestampMs);
|
|
15
24
|
if (Number.isNaN(date.getTime())) {
|
|
16
25
|
return null;
|
|
@@ -35,6 +44,51 @@ function getSeverity(leftPercent) {
|
|
|
35
44
|
return "danger";
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
function getQuotaLabels(key) {
|
|
48
|
+
return QUOTA_LABELS[key] || { text: key || "quota", compact: key || "Q" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeQuota(quota) {
|
|
52
|
+
if (!quota || typeof quota !== "object" || !Number.isFinite(quota.leftPercent)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const usedPercent = Number.isFinite(quota.usedPercent)
|
|
57
|
+
? quota.usedPercent
|
|
58
|
+
: Math.max(0, Math.min(100, 100 - quota.leftPercent));
|
|
59
|
+
const labels = getQuotaLabels(quota.key);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
key: quota.key || "token_5h",
|
|
63
|
+
label: labels.text,
|
|
64
|
+
compactLabel: labels.compact,
|
|
65
|
+
leftPercent: quota.leftPercent,
|
|
66
|
+
usedPercent,
|
|
67
|
+
leftText: `${quota.leftPercent}%`,
|
|
68
|
+
usedText: `${usedPercent}%`,
|
|
69
|
+
nextResetTime: Number.isFinite(quota.nextResetTime) ? quota.nextResetTime : null
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeQuotasFromResult(result) {
|
|
74
|
+
if (Array.isArray(result.quotas) && result.quotas.length > 0) {
|
|
75
|
+
return result.quotas.map(normalizeQuota).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.display === "percent" && Number.isFinite(result.leftPercent)) {
|
|
79
|
+
return [
|
|
80
|
+
normalizeQuota({
|
|
81
|
+
key: "token_5h",
|
|
82
|
+
leftPercent: result.leftPercent,
|
|
83
|
+
usedPercent: result.usedPercent,
|
|
84
|
+
nextResetTime: result.nextResetTime
|
|
85
|
+
})
|
|
86
|
+
].filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
38
92
|
export function buildStatusViewModel(result) {
|
|
39
93
|
if (!result || typeof result !== "object") {
|
|
40
94
|
return { kind: "unavailable" };
|
|
@@ -48,59 +102,26 @@ export function buildStatusViewModel(result) {
|
|
|
48
102
|
return { kind: "unavailable" };
|
|
49
103
|
}
|
|
50
104
|
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
105
|
+
const quotas = normalizeQuotasFromResult(result);
|
|
106
|
+
if (quotas.length === 0) {
|
|
53
107
|
return { kind: "unavailable" };
|
|
54
108
|
}
|
|
55
109
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// view model so the renderer can stay display-format-focused.
|
|
74
|
-
const usedValue =
|
|
75
|
-
Number.isFinite(result.total) && Number.isFinite(result.remaining)
|
|
76
|
-
? result.total - result.remaining
|
|
77
|
-
: null;
|
|
78
|
-
const leftPercent =
|
|
79
|
-
Number.isFinite(result.total) && result.total > 0 && Number.isFinite(result.remaining)
|
|
80
|
-
? Math.round((result.remaining / result.total) * 100)
|
|
81
|
-
: null;
|
|
82
|
-
const usedPercent =
|
|
83
|
-
Number.isFinite(leftPercent) ? Math.max(0, Math.min(100, 100 - leftPercent)) : null;
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
kind: "success",
|
|
87
|
-
display: "absolute",
|
|
88
|
-
levelLabel: formatLevel(result.level),
|
|
89
|
-
compactLabel: "GLM",
|
|
90
|
-
resetText,
|
|
91
|
-
leftPercent,
|
|
92
|
-
usedPercent,
|
|
93
|
-
leftText:
|
|
94
|
-
Number.isFinite(result.remaining) && Number.isFinite(result.total)
|
|
95
|
-
? `${result.remaining}/${result.total}`
|
|
96
|
-
: null,
|
|
97
|
-
usedText:
|
|
98
|
-
Number.isFinite(usedValue) && Number.isFinite(result.total)
|
|
99
|
-
? `${usedValue}/${result.total}`
|
|
100
|
-
: null,
|
|
101
|
-
severity: getSeverity(leftPercent)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { kind: "unavailable" };
|
|
110
|
+
const primaryQuota =
|
|
111
|
+
quotas.find((quota) => quota.key === result.primaryQuotaKey) ||
|
|
112
|
+
quotas.find((quota) => quota.key === "token_5h") ||
|
|
113
|
+
quotas[0];
|
|
114
|
+
const secondaryQuota = quotas.find((quota) => quota !== primaryQuota) || null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
kind: "success",
|
|
118
|
+
levelLabel: formatLevel(result.level),
|
|
119
|
+
compactLabel: "GLM",
|
|
120
|
+
primaryQuota,
|
|
121
|
+
secondaryQuota,
|
|
122
|
+
leftPercent: primaryQuota.leftPercent,
|
|
123
|
+
usedPercent: primaryQuota.usedPercent,
|
|
124
|
+
resetText: formatResetTime(primaryQuota.nextResetTime),
|
|
125
|
+
severity: getSeverity(primaryQuota.leftPercent)
|
|
126
|
+
};
|
|
106
127
|
}
|
package/src/shared/config.js
CHANGED
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_BAR_WIDTH,
|
|
7
7
|
DEFAULT_CACHE_TTL_MS,
|
|
8
|
+
DEFAULT_CN_BASE_URL,
|
|
9
|
+
DEFAULT_INTL_BASE_URL,
|
|
8
10
|
DEFAULT_DISPLAY_MODE,
|
|
9
11
|
DEFAULT_PALETTE,
|
|
10
12
|
DEFAULT_QUOTA_URL,
|
|
@@ -45,14 +47,19 @@ function deriveQuotaUrl(baseUrl) {
|
|
|
45
47
|
|
|
46
48
|
try {
|
|
47
49
|
const parsedBaseUrl = new URL(baseUrl);
|
|
48
|
-
const
|
|
50
|
+
const host = parsedBaseUrl.host;
|
|
51
|
+
|
|
52
|
+
if (host.includes("api.z.ai")) {
|
|
53
|
+
return `${DEFAULT_INTL_BASE_URL}/api/monitor/usage/quota/limit`;
|
|
54
|
+
}
|
|
49
55
|
|
|
50
56
|
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
host.includes("open.bigmodel.cn") ||
|
|
58
|
+
host.includes("dev.bigmodel.cn") ||
|
|
59
|
+
host === "bigmodel.cn" ||
|
|
60
|
+
host.endsWith(".bigmodel.cn")
|
|
54
61
|
) {
|
|
55
|
-
return `${
|
|
62
|
+
return `${DEFAULT_CN_BASE_URL}/api/monitor/usage/quota/limit`;
|
|
56
63
|
}
|
|
57
64
|
} catch {
|
|
58
65
|
return "";
|
package/src/shared/constants.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_QUOTA_URL = "https://bigmodel.cn/api/monitor/usage/quota/limit";
|
|
2
|
+
export const DEFAULT_CN_BASE_URL = "https://open.bigmodel.cn";
|
|
3
|
+
export const DEFAULT_INTL_BASE_URL = "https://api.z.ai";
|
|
2
4
|
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
3
5
|
export const DEFAULT_CACHE_TTL_MS = 300_000;
|
|
4
6
|
export const DEFAULT_DISPLAY_MODE = "left";
|
|
@@ -13,8 +15,8 @@ export const THEMES = ["plain", "ansi"];
|
|
|
13
15
|
export const PALETTES = ["dark", "mono"];
|
|
14
16
|
|
|
15
17
|
export const STATUS_BAR_CHARACTERS = {
|
|
16
|
-
filled: "
|
|
17
|
-
empty: "
|
|
18
|
+
filled: "█",
|
|
19
|
+
empty: "░"
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export const TOOL_CONFIG_SCHEMA_VERSION = 1;
|