glm-quota-line 0.8.0 → 0.9.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 +8 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/commands.js +15 -0
- package/src/core/quota/cache.js +9 -18
- package/src/core/quota/service.js +31 -34
- package/src/core/status/viewModel.js +3 -1
- package/src/shared/constants.js +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- Replaced progressive tier-based backoff with diamond-shaped refresh strategy: high quota (80–100%) refreshes every 2 min, medium (30–79%) every 5 min, low (0–29%) every 2 min — frequent updates when usage is active or quota is near exhaustion, relaxed in between
|
|
6
|
+
- Added failure-type-aware retry TTLs: rate-limited (429) retries after 3 min, unavailable retries after 2 min, instead of reusing the quota-based TTL
|
|
7
|
+
- Removed `refreshCount`/`tierIndex` cache fields and `advanceTier()` — cache format is now simpler, and TTL is derived purely from quota percentage
|
|
8
|
+
- Aligned severity threshold (danger/warn boundary) from hardcoded 25% to `LOW_QUOTA_THRESHOLD` (30%), consistent with the refresh band boundary
|
|
9
|
+
- Unknown CLI commands and invalid config subcommands now print an error and exit with code 1 instead of silently showing quota status
|
|
10
|
+
|
|
3
11
|
## 0.8.0
|
|
4
12
|
|
|
5
13
|
- Added progressive refresh backoff: new sessions start at 3-minute intervals, advance to 5 minutes after 5 refreshes, and cap at 10 minutes after 5 more — giving new users fast feedback while reducing API pressure in long sessions
|
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ glm-quota-line config set <style|display|theme|palette|auth-token|base-url> <val
|
|
|
106
106
|
glm-quota-line config unset <key>
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
运行 `glm-quota-line --help` 查看完整说明。
|
|
110
110
|
|
|
111
111
|
`glm-quota-line --version` 可直接查看当前安装版本;`check-update` 只检查并提示升级命令,不会自动修改环境。
|
|
112
112
|
|
package/package.json
CHANGED
package/src/cli/commands.js
CHANGED
|
@@ -149,11 +149,13 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
149
149
|
if (command === "config" && subcommand === "set") {
|
|
150
150
|
const configKey = CONFIG_KEYS[key];
|
|
151
151
|
if (!configKey) {
|
|
152
|
+
process.exitCode = 1;
|
|
152
153
|
output.write("Supported config keys: style, display, theme, palette, auth-token, base-url\n");
|
|
153
154
|
return true;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
if (!configKey.validate(value)) {
|
|
158
|
+
process.exitCode = 1;
|
|
157
159
|
output.write(`${configKey.invalidMessage}\n`);
|
|
158
160
|
return true;
|
|
159
161
|
}
|
|
@@ -168,6 +170,7 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
168
170
|
if (command === "config" && subcommand === "unset") {
|
|
169
171
|
const configKey = CONFIG_KEYS[key];
|
|
170
172
|
if (!configKey) {
|
|
173
|
+
process.exitCode = 1;
|
|
171
174
|
output.write("Supported config keys: style, display, theme, palette, auth-token, base-url\n");
|
|
172
175
|
return true;
|
|
173
176
|
}
|
|
@@ -177,5 +180,17 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
177
180
|
return true;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
if (command === "config") {
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
output.write("Supported config subcommands: show, set, unset\n");
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (command) {
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
output.write(`Unknown command: ${command}\nRun 'glm-quota-line -h' for usage.\n`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
return false;
|
|
181
196
|
}
|
package/src/core/quota/cache.js
CHANGED
|
@@ -80,6 +80,10 @@ function normalizeCache(parsed) {
|
|
|
80
80
|
const savedAt = asFiniteNumber(parsed.savedAt);
|
|
81
81
|
const lastAttemptAt = asFiniteNumber(parsed.lastAttemptAt) ?? savedAt;
|
|
82
82
|
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
|
|
83
|
+
const lastFailureKind =
|
|
84
|
+
parsed.lastFailureKind === "rate_limited" || parsed.lastFailureKind === "unavailable"
|
|
85
|
+
? parsed.lastFailureKind
|
|
86
|
+
: null;
|
|
83
87
|
|
|
84
88
|
if (result && savedAt === null) {
|
|
85
89
|
return null;
|
|
@@ -89,21 +93,11 @@ function normalizeCache(parsed) {
|
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
const refreshCount =
|
|
93
|
-
typeof parsed.refreshCount === "number" && Number.isFinite(parsed.refreshCount)
|
|
94
|
-
? Math.max(0, Math.floor(parsed.refreshCount))
|
|
95
|
-
: 0;
|
|
96
|
-
const tierIndex =
|
|
97
|
-
typeof parsed.tierIndex === "number" && Number.isFinite(parsed.tierIndex)
|
|
98
|
-
? Math.max(0, Math.floor(parsed.tierIndex))
|
|
99
|
-
: 0;
|
|
100
|
-
|
|
101
96
|
return {
|
|
102
97
|
savedAt,
|
|
103
98
|
lastAttemptAt,
|
|
104
99
|
sessionId,
|
|
105
|
-
|
|
106
|
-
tierIndex,
|
|
100
|
+
lastFailureKind,
|
|
107
101
|
result
|
|
108
102
|
};
|
|
109
103
|
}
|
|
@@ -114,8 +108,7 @@ async function writeCache(cacheFilePath, cache) {
|
|
|
114
108
|
...(Number.isFinite(cache.savedAt) ? { savedAt: cache.savedAt } : {}),
|
|
115
109
|
...(Number.isFinite(cache.lastAttemptAt) ? { lastAttemptAt: cache.lastAttemptAt } : {}),
|
|
116
110
|
...(cache.sessionId ? { sessionId: cache.sessionId } : {}),
|
|
117
|
-
|
|
118
|
-
tierIndex: cache.tierIndex ?? 0,
|
|
111
|
+
...(cache.lastFailureKind ? { lastFailureKind: cache.lastFailureKind } : {}),
|
|
119
112
|
...(cache.result ? { result: cache.result } : {})
|
|
120
113
|
},
|
|
121
114
|
null,
|
|
@@ -142,21 +135,19 @@ export async function writeSuccessCache(cacheFilePath, result, options = {}) {
|
|
|
142
135
|
savedAt: now,
|
|
143
136
|
lastAttemptAt: now,
|
|
144
137
|
sessionId: options.sessionId || "",
|
|
145
|
-
|
|
146
|
-
tierIndex: options.tierIndex ?? 0,
|
|
138
|
+
lastFailureKind: null,
|
|
147
139
|
result
|
|
148
140
|
});
|
|
149
141
|
}
|
|
150
142
|
|
|
151
|
-
export async function
|
|
143
|
+
export async function writeFailureCache(cacheFilePath, cached, options = {}) {
|
|
152
144
|
const now = options.now ?? Date.now();
|
|
153
145
|
|
|
154
146
|
await writeCache(cacheFilePath, {
|
|
155
147
|
savedAt: cached?.savedAt ?? null,
|
|
156
148
|
lastAttemptAt: now,
|
|
157
149
|
sessionId: options.sessionId || cached?.sessionId || "",
|
|
158
|
-
|
|
159
|
-
tierIndex: cached?.tierIndex ?? 0,
|
|
150
|
+
lastFailureKind: options.failureKind || "unavailable",
|
|
160
151
|
result: cached?.result ?? null
|
|
161
152
|
});
|
|
162
153
|
}
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readCache,
|
|
3
|
-
|
|
3
|
+
writeFailureCache,
|
|
4
4
|
writeSuccessCache
|
|
5
5
|
} from "./cache.js";
|
|
6
6
|
import { fetchQuota } from "./fetch.js";
|
|
7
7
|
import { parseQuotaResponse } from "./parse.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
RATE_LIMIT_RETRY_TTL_MS,
|
|
10
|
+
REFRESH_BANDS,
|
|
11
|
+
UNAVAILABLE_RETRY_TTL_MS
|
|
11
12
|
} from "../../shared/constants.js";
|
|
12
13
|
|
|
13
14
|
function getCachedStatus(cached) {
|
|
14
15
|
return cached?.result ?? { kind: "unavailable" };
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function
|
|
18
|
-
if (Number.isFinite(leftPercent)
|
|
19
|
-
return
|
|
18
|
+
function getRefreshTtlMs(leftPercent) {
|
|
19
|
+
if (!Number.isFinite(leftPercent)) {
|
|
20
|
+
return REFRESH_BANDS[1].ttlMs;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
return
|
|
23
|
+
const matchedBand = REFRESH_BANDS.find((band) => leftPercent >= band.minLeftPercent);
|
|
24
|
+
return matchedBand ? matchedBand.ttlMs : REFRESH_BANDS[REFRESH_BANDS.length - 1].ttlMs;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
function getEffectiveTtlMs(cached) {
|
|
28
|
+
if (cached?.lastFailureKind === "rate_limited") {
|
|
29
|
+
return RATE_LIMIT_RETRY_TTL_MS;
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
+
if (cached?.lastFailureKind === "unavailable") {
|
|
33
|
+
return UNAVAILABLE_RETRY_TTL_MS;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
return
|
|
36
|
+
return getRefreshTtlMs(cached?.result?.leftPercent);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function shouldRefreshQuota(cached, cacheTtlMs, now, sessionId) {
|
|
@@ -60,9 +62,7 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const cached = await readCache(config.cacheFilePath);
|
|
63
|
-
const
|
|
64
|
-
const leftPercent = cached?.result?.leftPercent;
|
|
65
|
-
const effectiveTtlMs = getEffectiveTtlMs(tierIndex, leftPercent);
|
|
65
|
+
const effectiveTtlMs = getEffectiveTtlMs(cached);
|
|
66
66
|
|
|
67
67
|
const shouldRefresh = options.forceRefresh
|
|
68
68
|
? true
|
|
@@ -76,23 +76,9 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
76
76
|
const parsed = parseQuotaResponse(response);
|
|
77
77
|
|
|
78
78
|
if (parsed.kind === "success") {
|
|
79
|
-
const isNewSession = Boolean(sessionId) && cached?.sessionId !== sessionId;
|
|
80
|
-
const isLowQuota = Number.isFinite(parsed.leftPercent) && parsed.leftPercent < LOW_QUOTA_THRESHOLD;
|
|
81
|
-
|
|
82
|
-
let nextTier;
|
|
83
|
-
if (isNewSession) {
|
|
84
|
-
nextTier = { refreshCount: 1, tierIndex: 0 };
|
|
85
|
-
} else if (isLowQuota) {
|
|
86
|
-
nextTier = { refreshCount: cached?.refreshCount ?? 0, tierIndex };
|
|
87
|
-
} else {
|
|
88
|
-
nextTier = advanceTier(cached?.refreshCount ?? 0, tierIndex);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
79
|
await writeSuccessCache(config.cacheFilePath, parsed, {
|
|
92
80
|
now,
|
|
93
|
-
sessionId
|
|
94
|
-
refreshCount: nextTier.refreshCount,
|
|
95
|
-
tierIndex: nextTier.tierIndex
|
|
81
|
+
sessionId
|
|
96
82
|
});
|
|
97
83
|
return parsed;
|
|
98
84
|
}
|
|
@@ -102,16 +88,27 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
if (parsed.kind === "rate_limited") {
|
|
105
|
-
await
|
|
91
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
106
92
|
now,
|
|
107
|
-
sessionId
|
|
93
|
+
sessionId,
|
|
94
|
+
failureKind: "rate_limited"
|
|
108
95
|
});
|
|
109
96
|
return getCachedStatus(cached);
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
if (cached?.result) {
|
|
100
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
101
|
+
now,
|
|
102
|
+
sessionId,
|
|
103
|
+
failureKind: "unavailable"
|
|
104
|
+
});
|
|
113
105
|
return cached.result;
|
|
114
106
|
}
|
|
115
107
|
|
|
108
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
109
|
+
now,
|
|
110
|
+
sessionId,
|
|
111
|
+
failureKind: "unavailable"
|
|
112
|
+
});
|
|
116
113
|
return parsed;
|
|
117
114
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LOW_QUOTA_THRESHOLD } from "../../shared/constants.js";
|
|
2
|
+
|
|
1
3
|
function padTwoDigits(value) {
|
|
2
4
|
return String(value).padStart(2, "0");
|
|
3
5
|
}
|
|
@@ -37,7 +39,7 @@ function getSeverity(leftPercent) {
|
|
|
37
39
|
return "good";
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
if (leftPercent >=
|
|
42
|
+
if (leftPercent >= LOW_QUOTA_THRESHOLD) {
|
|
41
43
|
return "warn";
|
|
42
44
|
}
|
|
43
45
|
|
package/src/shared/constants.js
CHANGED
|
@@ -4,13 +4,15 @@ export const DEFAULT_INTL_BASE_URL = "https://api.z.ai";
|
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
5
5
|
export const DEFAULT_CACHE_TTL_MS = 600_000;
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
7
|
+
export const REFRESH_BANDS = [
|
|
8
|
+
{ minLeftPercent: 80, ttlMs: 120_000 }, // 2 min
|
|
9
|
+
{ minLeftPercent: 30, ttlMs: 300_000 }, // 5 min
|
|
10
|
+
{ minLeftPercent: 0, ttlMs: 120_000 } // 2 min
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
export const LOW_QUOTA_THRESHOLD = 30;
|
|
14
|
+
export const RATE_LIMIT_RETRY_TTL_MS = 180_000;
|
|
15
|
+
export const UNAVAILABLE_RETRY_TTL_MS = 120_000;
|
|
14
16
|
export const DEFAULT_DISPLAY_MODE = "left";
|
|
15
17
|
export const DEFAULT_STYLE = "text";
|
|
16
18
|
export const DEFAULT_BAR_WIDTH = 10;
|