@iasiv5/dsh-quota-watch 0.0.1 → 0.0.3
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 +9 -7
- package/lib/client.js +245 -130
- package/package.json +1 -1
- package/src/client.mjs +224 -125
- package/src/core/adapters.mjs +74 -5
- package/src/host.mjs +66 -2
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- 在侧栏 Settings
|
|
10
|
-
- GLM
|
|
11
|
-
- GitHub Copilot
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
- 在侧栏 Settings 行上方显示一张常显摘要卡(无折叠按钮):GLM 与 GitHub Copilot 各占一行,统一显示**已用百分比**(Copilot 为 `100 − percent_remaining` 的单值反转),已用超过 80% 进度条转橙、超过 95% 转红。
|
|
10
|
+
- GLM 摘要行固定显示 5 小时窗口已用百分比,行尾小字为当日 token 用量;点击行展开详情:今日 tokens / 调用次数双大数字、全部窗口(5 小时 / 每周 / 每月 / MCP 月度 `TIME_LIMIT`)的百分比与重置时间、分模型用量与更新时间。同一时间至多展开一个详情。
|
|
11
|
+
- GitHub Copilot 摘要行仅在 GitHub 返回有限、有效的 `remaining` 且带 `percent_remaining` 时显示;点击展开可用额度、已用百分比、`credits_used` 与重置时间。`credits_used` 绝不用于推算余额,缺失有效百分比时该行不渲染。
|
|
12
|
+
- 今日 tokens / 调用次数按宿主本地时区的自然日(`00:00:00 → 当前时刻`)查询;不带滚动窗口语义。
|
|
13
|
+
- 凭据缺失的 provider 不渲染摘要行(两者皆缺时整卡隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记。
|
|
14
|
+
- 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停;刷新按钮可立即请求宿主探测。
|
|
14
15
|
|
|
15
16
|
## 兼容与配置
|
|
16
17
|
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
## 数据来源与语义
|
|
23
24
|
|
|
24
|
-
- GLM 查询 `https://open.bigmodel.cn/api/monitor/usage/quota/limit` 或 `https://api.z.ai/api/monitor/usage/quota/limit
|
|
25
|
+
- GLM 查询 `https://open.bigmodel.cn/api/monitor/usage/quota/limit` 或 `https://api.z.ai/api/monitor/usage/quota/limit`(窗口百分比),以及同域 `/api/monitor/usage/model-usage?startTime=…&endTime=…`(今日 tokens 与调用次数,时间格式 `yyyy-MM-dd HH:mm:ss`)。两个接口均按 raw API key 鉴权,不加 Bearer 前缀。
|
|
25
26
|
- Copilot 查询 `https://api.github.com/copilot_internal/user`。这是 GitHub Copilot 内部、非公开稳定契约的端点,字段可能变更;插件在无法确认额度含义时显示不可用,不猜测或计算余额。
|
|
26
27
|
- 对企业 / 组织管理的 Copilot seat,GitHub AI Credits 可能属于共享计费池。侧栏呈现的是 GitHub 用户 quota snapshot 中的可用额度,不保证等同组织账单的总余额。
|
|
27
28
|
- 凭据只在 DSH 宿主进程解析并用于 HTTPS 请求;浏览器只接收规范化后的额度字段。插件不把 key、OAuth grant 或原始 provider 响应写入磁盘,也不保存额度历史。
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
- API 路由只允许 loopback 或已配对的浏览器访问;响应禁用缓存。
|
|
32
33
|
- 本仓库、测试 fixtures 与 npm 包不得包含任何真实 key、OAuth grant、临时 token、原始账号响应或个人余额。测试仅使用合成响应。
|
|
33
34
|
- npm 包采用显式 `files` 白名单;`npm run security:check` 会扫描工作树和实际 pack 文件,诊断只报告路径和规则名,不打印匹配值。
|
|
34
|
-
- 首次 bootstrap 发布真实插件 `0.0.1`
|
|
35
|
+
- 首次 bootstrap 发布真实插件 `0.0.1` 使用 npm web-login;后续 tag release 使用 GitHub Actions OIDC 与 provenance,版本发布者身份为 GitHub Actions,不保存长期 npm token。
|
|
36
|
+
- OIDC 不会改写已发布的 `0.0.1` 元数据,也不负责移除 Registry 级 maintainer 字段;源码和 npm tarball 不写入发布账号邮箱。
|
|
35
37
|
|
|
36
38
|
## 安装
|
|
37
39
|
|
package/lib/client.js
CHANGED
|
@@ -50,67 +50,119 @@ var PROVIDER_IDS = Object.freeze({
|
|
|
50
50
|
});
|
|
51
51
|
var CLIENT_POLL_INTERVAL_MS = 3e4;
|
|
52
52
|
|
|
53
|
+
// src/core/adapters.mjs
|
|
54
|
+
var GLM_HOSTS = Object.freeze({
|
|
55
|
+
"zai-coding-cn": "open.bigmodel.cn",
|
|
56
|
+
zai: "api.z.ai",
|
|
57
|
+
"zai-coding": "api.z.ai"
|
|
58
|
+
});
|
|
59
|
+
var GLM_WINDOW_KEYS = Object.freeze({ 3: "5h", 5: "month", 6: "week" });
|
|
60
|
+
var COPILOT_HEADERS = Object.freeze({
|
|
61
|
+
accept: "application/json",
|
|
62
|
+
"user-agent": "GitHubCopilotChat/0.35.0",
|
|
63
|
+
"editor-version": "vscode/1.107.0",
|
|
64
|
+
"editor-plugin-version": "copilot-chat/0.35.0",
|
|
65
|
+
"copilot-integration-id": "vscode-chat"
|
|
66
|
+
});
|
|
67
|
+
function asNumber(value) {
|
|
68
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
69
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
70
|
+
const parsed = Number(value);
|
|
71
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function clampPercent(value) {
|
|
76
|
+
return Math.max(0, Math.min(100, value));
|
|
77
|
+
}
|
|
78
|
+
function remainingToUsed(percentRemaining) {
|
|
79
|
+
const value = asNumber(percentRemaining);
|
|
80
|
+
if (value === void 0 || value < 0 || value > 100) return void 0;
|
|
81
|
+
return clampPercent(Math.round((100 - value) * 100) / 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
// src/client.mjs
|
|
54
85
|
var name = "quota-watch-client";
|
|
55
86
|
var inject = [];
|
|
56
87
|
var CARD_SELECTOR = "[data-dsh-quota-watch-card]";
|
|
57
|
-
var COLLAPSE_KEY = "dsh-quota-watch:collapsed";
|
|
58
88
|
var FETCH_TIMEOUT_MS = 15e3;
|
|
59
89
|
var COPY = {
|
|
60
90
|
zh: {
|
|
61
91
|
title: "\u5957\u9910\u76D1\u63A7",
|
|
62
92
|
refresh: "\u5237\u65B0",
|
|
63
|
-
collapse: "\u6536\u8D77",
|
|
64
|
-
expand: "\u5C55\u5F00",
|
|
65
93
|
loading: "\u8BFB\u53D6\u4E2D",
|
|
66
94
|
updated: "\u66F4\u65B0",
|
|
67
|
-
notConfigured: "\u672A\u914D\u7F6E",
|
|
68
|
-
noPlan: "\u6682\u65E0\u5957\u9910\u7A97\u53E3",
|
|
69
95
|
failed: "\u67E5\u8BE2\u5931\u8D25",
|
|
70
96
|
stale: "\u6570\u636E\u53EF\u80FD\u5DF2\u8FC7\u671F",
|
|
71
97
|
requestFailed: "\u5237\u65B0\u5931\u8D25\uFF0C\u663E\u793A\u4E0A\u6B21\u6210\u529F\u6570\u636E",
|
|
72
98
|
available: "\u53EF\u7528\u989D\u5EA6",
|
|
73
99
|
used: "\u672C\u671F\u5DF2\u7528",
|
|
74
|
-
balanceUnavailable: "\u4F59\u989D\u6682\u4E0D\u53EF\u7528",
|
|
75
100
|
reset: "\u91CD\u7F6E",
|
|
76
|
-
|
|
101
|
+
todayTokens: "\u4ECA\u65E5 tokens",
|
|
102
|
+
todayCalls: "\u4ECA\u65E5\u8C03\u7528",
|
|
103
|
+
windows: {
|
|
104
|
+
"5h": "5 \u5C0F\u65F6\u7A97\u53E3 \xB7 \u5DF2\u7528",
|
|
105
|
+
week: "\u6BCF\u5468\u7A97\u53E3 \xB7 \u5DF2\u7528",
|
|
106
|
+
month: "\u6BCF\u6708\u7A97\u53E3 \xB7 \u5DF2\u7528",
|
|
107
|
+
mcp: "MCP\uFF08\u6708\uFF09\xB7 \u5DF2\u7528"
|
|
108
|
+
}
|
|
77
109
|
},
|
|
78
110
|
en: {
|
|
79
111
|
title: "Quota Watch",
|
|
80
112
|
refresh: "Refresh",
|
|
81
|
-
collapse: "Collapse",
|
|
82
|
-
expand: "Expand",
|
|
83
113
|
loading: "Loading",
|
|
84
114
|
updated: "Updated",
|
|
85
|
-
notConfigured: "Not configured",
|
|
86
|
-
noPlan: "No plan windows",
|
|
87
115
|
failed: "Query failed",
|
|
88
116
|
stale: "Data may be stale",
|
|
89
117
|
requestFailed: "Refresh failed; showing last successful data",
|
|
90
118
|
available: "Available credits",
|
|
91
119
|
used: "Used this cycle",
|
|
92
|
-
balanceUnavailable: "Balance unavailable",
|
|
93
120
|
reset: "Resets",
|
|
94
|
-
|
|
121
|
+
todayTokens: "Today tokens",
|
|
122
|
+
todayCalls: "Calls today",
|
|
123
|
+
windows: {
|
|
124
|
+
"5h": "5-hour window \xB7 used",
|
|
125
|
+
week: "Weekly window \xB7 used",
|
|
126
|
+
month: "Monthly window \xB7 used",
|
|
127
|
+
mcp: "MCP (month) \xB7 used"
|
|
128
|
+
}
|
|
95
129
|
}
|
|
96
130
|
};
|
|
97
131
|
var STYLE_TEXT = `
|
|
98
132
|
[data-dsh-quota-watch-card] { box-sizing: border-box; width: 100%; margin: 0 0 8px; color: var(--dsw-alias-label-primary, inherit); font: inherit; }
|
|
99
133
|
[data-dsh-quota-watch-card] *, [data-dsh-quota-watch-card] *::before, [data-dsh-quota-watch-card] *::after { box-sizing: border-box; }
|
|
100
|
-
.dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap:
|
|
134
|
+
.dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
101
135
|
.dqw-head { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
102
136
|
.dqw-title { flex: 1; min-width: 0; margin: 0; font-size: 12px; font-weight: 600; line-height: 18px; }
|
|
103
137
|
.dqw-button { border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: 11px; line-height: 18px; padding: 1px 4px; }
|
|
104
138
|
.dqw-button:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.16)); }
|
|
105
139
|
.dqw-button:disabled { cursor: wait; opacity: .6; }
|
|
106
140
|
.dqw-meta { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
107
|
-
.dqw-
|
|
108
|
-
.dqw-
|
|
141
|
+
.dqw-body { display: flex; flex-direction: column; }
|
|
142
|
+
.dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 7px 6px; margin: 1px -2px 2px; border-radius: 8px; }
|
|
143
|
+
.dqw-row-summary[data-expandable] { cursor: pointer; }
|
|
144
|
+
.dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
|
|
145
|
+
.dqw-row-summary:focus-visible { outline: 1px solid var(--dsw-alias-button-primary-fill, #5b8def); outline-offset: -1px; }
|
|
146
|
+
.dqw-row-summary.is-error { cursor: default; }
|
|
147
|
+
.dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
|
|
148
|
+
.dqw-bar { display: block; flex: 1; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
149
|
+
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
150
|
+
.dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
|
|
151
|
+
.dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
|
|
152
|
+
.dqw-pct { font-size: 10.5px; line-height: 14px; font-variant-numeric: tabular-nums; width: 30px; text-align: right; flex: none; }
|
|
153
|
+
.dqw-extra { font-size: 10px; line-height: 14px; opacity: .72; font-variant-numeric: tabular-nums; flex: none; }
|
|
154
|
+
.dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
|
|
155
|
+
.dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
|
|
156
|
+
.dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
157
|
+
.dqw-detail { padding: 2px 6px 6px; display: flex; flex-direction: column; gap: 7px; }
|
|
158
|
+
.dqw-bignums { display: flex; gap: 6px; }
|
|
159
|
+
.dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 6px 8px; }
|
|
160
|
+
.dqw-big .dqw-big-value { font-size: 16px; font-weight: 650; line-height: 20px; font-variant-numeric: tabular-nums; }
|
|
161
|
+
.dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
|
|
109
162
|
.dqw-row { display: flex; flex-direction: column; gap: 2px; }
|
|
110
163
|
.dqw-row-head { display: flex; justify-content: space-between; gap: 5px; font-size: 10px; line-height: 14px; }
|
|
111
|
-
.dqw-bar { display: block; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
112
|
-
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
113
164
|
.dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
165
|
+
.dqw-models { margin: 0; font-size: 10px; line-height: 15px; opacity: .8; border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; }
|
|
114
166
|
.dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
|
|
115
167
|
.dqw-credit { margin: 0; font-size: 10px; line-height: 14px; font-variant-numeric: tabular-nums; }
|
|
116
168
|
`;
|
|
@@ -121,6 +173,13 @@ function formatNumber(value, locale) {
|
|
|
121
173
|
if (typeof value !== "number" || !Number.isFinite(value)) return "";
|
|
122
174
|
return new Intl.NumberFormat(locale === "zh" ? "zh-CN" : "en-US", { maximumFractionDigits: 1 }).format(value);
|
|
123
175
|
}
|
|
176
|
+
function formatCompact(value, locale) {
|
|
177
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "";
|
|
178
|
+
return new Intl.NumberFormat(locale === "zh" ? "zh-CN" : "en-US", {
|
|
179
|
+
notation: "compact",
|
|
180
|
+
maximumFractionDigits: 1
|
|
181
|
+
}).format(value);
|
|
182
|
+
}
|
|
124
183
|
function formatPercent(value, locale) {
|
|
125
184
|
if (typeof value !== "number" || !Number.isFinite(value)) return "";
|
|
126
185
|
const clamped = Math.max(0, Math.min(100, value));
|
|
@@ -155,101 +214,47 @@ function progress(doc, label, percent, locale) {
|
|
|
155
214
|
bar.setAttribute("aria-valuemax", "100");
|
|
156
215
|
bar.setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, percent))));
|
|
157
216
|
const fill = doc.createElement("span");
|
|
158
|
-
fill.className =
|
|
217
|
+
fill.className = `dqw-bar-fill${percent > 95 ? " danger" : percent > 80 ? " warn" : ""}`;
|
|
159
218
|
fill.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
|
160
219
|
bar.append(fill);
|
|
161
220
|
row.append(head, bar);
|
|
162
221
|
return row;
|
|
163
222
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
function renderGlm(doc, section, provider, copy, locale) {
|
|
180
|
-
const content = section.querySelector('[data-role="content"]');
|
|
181
|
-
content.replaceChildren();
|
|
182
|
-
appendStaleError(doc, content, provider, copy);
|
|
183
|
-
if (provider.status === "missing") {
|
|
184
|
-
content.append(text(doc, "p", "dqw-muted", copy.notConfigured));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (provider.status === "loading") {
|
|
188
|
-
content.append(text(doc, "p", "dqw-muted", copy.loading));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (!provider.plan?.windows?.length) {
|
|
192
|
-
if (provider.status !== "error") content.append(text(doc, "p", "dqw-muted", copy.noPlan));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const planName = provider.plan.planName;
|
|
196
|
-
if (planName) content.append(text(doc, "p", "dqw-muted", planName));
|
|
197
|
-
for (const window of provider.plan.windows) {
|
|
198
|
-
const label = window.name || copy.windows[window.key] || window.key;
|
|
199
|
-
if (typeof window.percent === "number" && Number.isFinite(window.percent)) {
|
|
200
|
-
content.append(progress(doc, label, window.percent, locale));
|
|
201
|
-
} else {
|
|
202
|
-
content.append(text(doc, "p", "dqw-muted", label));
|
|
203
|
-
}
|
|
204
|
-
const reset = formatTime(window.resetsAt, locale);
|
|
205
|
-
if (reset) content.append(text(doc, "p", "dqw-muted", `${copy.reset}: ${reset}`));
|
|
206
|
-
}
|
|
223
|
+
function glmRowModel(provider) {
|
|
224
|
+
if (provider?.status === "missing") return null;
|
|
225
|
+
if (provider?.status === "error") return { kind: "error", label: "GLM", error: provider.error };
|
|
226
|
+
const windows = provider?.plan?.windows;
|
|
227
|
+
if (!Array.isArray(windows) || windows.length === 0) return null;
|
|
228
|
+
const five = windows.find((window) => window?.key === "5h");
|
|
229
|
+
const tokens = provider?.usage?.status === "ready" || provider?.usage?.status === "stale" ? provider?.usage?.totalTokens : void 0;
|
|
230
|
+
return {
|
|
231
|
+
kind: "data",
|
|
232
|
+
label: "GLM",
|
|
233
|
+
percent: five?.percent,
|
|
234
|
+
stale: provider.status === "stale",
|
|
235
|
+
error: provider.error,
|
|
236
|
+
extra: tokens
|
|
237
|
+
};
|
|
207
238
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (quota?.balanceAvailable === true) {
|
|
223
|
-
content.append(text(doc, "p", "dqw-credit", `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`));
|
|
224
|
-
if (typeof quota.percentRemaining === "number") {
|
|
225
|
-
content.append(progress(doc, copy.available, quota.percentRemaining, locale));
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
content.append(text(doc, "p", "dqw-muted", copy.balanceUnavailable));
|
|
229
|
-
}
|
|
230
|
-
if (typeof quota?.creditsUsed === "number") {
|
|
231
|
-
content.append(text(doc, "p", "dqw-muted", `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`));
|
|
232
|
-
}
|
|
233
|
-
const reset = formatTime(quota?.resetsAt, locale);
|
|
234
|
-
if (reset) content.append(text(doc, "p", "dqw-muted", `${copy.reset}: ${reset}`));
|
|
239
|
+
function copilotRowModel(provider) {
|
|
240
|
+
if (provider?.status === "missing") return null;
|
|
241
|
+
if (provider?.status === "error") return { kind: "error", label: "Copilot", error: provider.error };
|
|
242
|
+
const quota = provider?.quota;
|
|
243
|
+
if (quota?.balanceAvailable !== true) return null;
|
|
244
|
+
const used = remainingToUsed(quota.percentRemaining);
|
|
245
|
+
if (used === void 0) return null;
|
|
246
|
+
return {
|
|
247
|
+
kind: "data",
|
|
248
|
+
label: "Copilot",
|
|
249
|
+
percent: used,
|
|
250
|
+
stale: provider.status === "stale",
|
|
251
|
+
error: provider.error
|
|
252
|
+
};
|
|
235
253
|
}
|
|
236
254
|
function footArea(doc) {
|
|
237
|
-
const sidebar = doc.querySelector('[
|
|
255
|
+
const sidebar = doc.querySelector('[class*="sidebarCol"]');
|
|
238
256
|
return sidebar?.querySelector('[class*="footArea"]') ?? void 0;
|
|
239
257
|
}
|
|
240
|
-
function readCollapsed(win) {
|
|
241
|
-
try {
|
|
242
|
-
return win.localStorage.getItem(COLLAPSE_KEY) === "1";
|
|
243
|
-
} catch {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function writeCollapsed(win, value) {
|
|
248
|
-
try {
|
|
249
|
-
win.localStorage.setItem(COLLAPSE_KEY, value ? "1" : "0");
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
258
|
async function fetchJson(fetchImpl, path, method) {
|
|
254
259
|
const response = await fetchImpl(path, {
|
|
255
260
|
...method === "POST" ? { method: "POST" } : {},
|
|
@@ -287,40 +292,138 @@ function mountQuotaCard({
|
|
|
287
292
|
refreshButton.title = copy.refresh;
|
|
288
293
|
refreshButton.setAttribute("aria-label", copy.refresh);
|
|
289
294
|
refreshButton.dataset.action = "refresh";
|
|
290
|
-
|
|
291
|
-
collapseButton.type = "button";
|
|
292
|
-
collapseButton.dataset.action = "collapse";
|
|
295
|
+
header.append(refreshButton);
|
|
293
296
|
const body = doc.createElement("div");
|
|
294
297
|
body.className = "dqw-body";
|
|
295
298
|
const lastUpdated = text(doc, "p", "dqw-meta", copy.loading);
|
|
296
299
|
lastUpdated.dataset.role = "updated";
|
|
297
|
-
|
|
298
|
-
const copilotSection = providerSection(doc, "copilot", "GitHub Copilot");
|
|
299
|
-
body.append(lastUpdated, glmSection, copilotSection);
|
|
300
|
-
header.append(refreshButton, collapseButton);
|
|
300
|
+
body.append(lastUpdated);
|
|
301
301
|
card.append(header, body);
|
|
302
302
|
container.append(style, card);
|
|
303
|
-
let collapsed = readCollapsed(win);
|
|
304
303
|
let snapshot;
|
|
305
304
|
let requestSequence = 0;
|
|
306
305
|
let timer;
|
|
307
306
|
let disposed = false;
|
|
308
307
|
let queuedPlace = false;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
308
|
+
let openKey;
|
|
309
|
+
const fillClass = (percent) => percent > 95 ? "dqw-bar-fill danger" : percent > 80 ? "dqw-bar-fill warn" : "dqw-bar-fill";
|
|
310
|
+
const renderProvider = (key, model, provider) => {
|
|
311
|
+
if (model === null) return null;
|
|
312
|
+
const row = doc.createElement("div");
|
|
313
|
+
row.className = "dqw-row-summary";
|
|
314
|
+
row.dataset.dshQuotaWatchRow = key;
|
|
315
|
+
row.append(text(doc, "span", "dqw-label", model.label));
|
|
316
|
+
if (model.kind === "error") {
|
|
317
|
+
row.classList.add("is-error");
|
|
318
|
+
row.append(text(doc, "span", "dqw-errtext", model.error ? `${copy.failed}: ${model.error}` : copy.failed));
|
|
319
|
+
return [row];
|
|
320
|
+
}
|
|
321
|
+
row.setAttribute("role", "button");
|
|
322
|
+
row.tabIndex = 0;
|
|
323
|
+
row.setAttribute("aria-expanded", String(openKey === key));
|
|
324
|
+
row.dataset.expandable = "";
|
|
325
|
+
if (typeof model.percent === "number" && Number.isFinite(model.percent)) {
|
|
326
|
+
const bar = doc.createElement("span");
|
|
327
|
+
bar.className = "dqw-bar";
|
|
328
|
+
bar.setAttribute("role", "progressbar");
|
|
329
|
+
bar.setAttribute("aria-valuemin", "0");
|
|
330
|
+
bar.setAttribute("aria-valuemax", "100");
|
|
331
|
+
bar.setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, model.percent))));
|
|
332
|
+
const fill = text(doc, "span", fillClass(model.percent));
|
|
333
|
+
fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`;
|
|
334
|
+
bar.append(fill);
|
|
335
|
+
row.append(bar);
|
|
336
|
+
}
|
|
337
|
+
if (model.stale) {
|
|
338
|
+
const mark = text(doc, "span", "dqw-stale-mark", "\u26A0");
|
|
339
|
+
mark.dataset.dshQuotaWatchStale = "";
|
|
340
|
+
mark.title = model.error ?? copy.stale;
|
|
341
|
+
row.append(mark);
|
|
342
|
+
}
|
|
343
|
+
row.append(text(doc, "span", "dqw-pct", typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"));
|
|
344
|
+
if (model.extra !== void 0) {
|
|
345
|
+
const extra = text(doc, "span", "dqw-extra", formatCompact(model.extra, locale));
|
|
346
|
+
extra.dataset.dshQuotaWatchExtra = "";
|
|
347
|
+
row.append(extra);
|
|
348
|
+
}
|
|
349
|
+
row.append(text(doc, "span", "dqw-chev", openKey === key ? "\u25BE" : "\u25B8"));
|
|
350
|
+
const detail = doc.createElement("div");
|
|
351
|
+
detail.className = "dqw-detail";
|
|
352
|
+
detail.dataset.dshQuotaWatchDetail = key;
|
|
353
|
+
if (openKey === key) renderDetail(detail, key, provider);
|
|
354
|
+
return [row, detail];
|
|
355
|
+
};
|
|
356
|
+
function renderDetail(detail, key, provider) {
|
|
357
|
+
detail.replaceChildren();
|
|
358
|
+
if (provider?.status === "stale") {
|
|
359
|
+
detail.append(text(doc, "p", "dqw-error", `${copy.stale}${provider.error ? `: ${provider.error}` : ""}`));
|
|
360
|
+
}
|
|
361
|
+
if (key === "glm") {
|
|
362
|
+
const usage = provider?.usage;
|
|
363
|
+
const numbers = doc.createElement("div");
|
|
364
|
+
numbers.className = "dqw-bignums";
|
|
365
|
+
const tokensBig = doc.createElement("div");
|
|
366
|
+
tokensBig.className = "dqw-big";
|
|
367
|
+
tokensBig.append(
|
|
368
|
+
text(doc, "div", "dqw-big-value", formatCompact(usage?.totalTokens, locale)),
|
|
369
|
+
text(doc, "div", "dqw-big-label", copy.todayTokens)
|
|
370
|
+
);
|
|
371
|
+
const callsBig = doc.createElement("div");
|
|
372
|
+
callsBig.className = "dqw-big";
|
|
373
|
+
callsBig.append(
|
|
374
|
+
text(doc, "div", "dqw-big-value", formatNumber(usage?.totalCalls, locale)),
|
|
375
|
+
text(doc, "div", "dqw-big-label", copy.todayCalls)
|
|
376
|
+
);
|
|
377
|
+
numbers.append(tokensBig, callsBig);
|
|
378
|
+
detail.append(numbers);
|
|
379
|
+
for (const window of provider?.plan?.windows ?? []) {
|
|
380
|
+
const label = copy.windows[window?.key] ?? window?.key;
|
|
381
|
+
if (typeof window?.percent === "number" && Number.isFinite(window.percent)) {
|
|
382
|
+
detail.append(progress(doc, label, window.percent, locale));
|
|
383
|
+
} else {
|
|
384
|
+
detail.append(text(doc, "p", "dqw-muted", label));
|
|
385
|
+
}
|
|
386
|
+
const reset2 = formatTime(window?.resetsAt, locale);
|
|
387
|
+
if (reset2) detail.append(text(doc, "p", "dqw-muted", `${copy.reset}: ${reset2}`));
|
|
388
|
+
}
|
|
389
|
+
const models = Array.isArray(usage?.models) && usage.models.length > 0 ? usage.models.map((model) => `${model.name} ${formatCompact(model.tokens, locale)}`).join(" \xB7 ") : void 0;
|
|
390
|
+
if (models) detail.append(text(doc, "p", "dqw-models", models));
|
|
391
|
+
const updated = formatTime(provider?.updatedAt ? new Date(provider.updatedAt).toISOString() : "", locale);
|
|
392
|
+
if (updated) detail.append(text(doc, "p", "dqw-meta", `${copy.updated}: ${updated}`));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const quota = provider?.quota;
|
|
396
|
+
if (quota?.balanceAvailable === true) {
|
|
397
|
+
detail.append(text(doc, "p", "dqw-credit", `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`));
|
|
398
|
+
const used = remainingToUsed(quota.percentRemaining);
|
|
399
|
+
if (used !== void 0) detail.append(progress(doc, copy.used, used, locale));
|
|
400
|
+
if (typeof quota.creditsUsed === "number" && quota.creditsUsed >= 0) {
|
|
401
|
+
detail.append(text(doc, "p", "dqw-muted", `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`));
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
detail.append(text(doc, "p", "dqw-muted", copy.stale));
|
|
405
|
+
}
|
|
406
|
+
const reset = formatTime(quota?.resetsAt, locale);
|
|
407
|
+
if (reset) detail.append(text(doc, "p", "dqw-muted", `${copy.reset}: ${reset}`));
|
|
408
|
+
}
|
|
409
|
+
const renderAll = () => {
|
|
410
|
+
const providers = snapshot?.providers ?? [];
|
|
411
|
+
const glm = providers.find((provider) => provider?.key === "glm");
|
|
412
|
+
const copilot = providers.find((provider) => provider?.key === "copilot");
|
|
413
|
+
const glmParts = renderProvider("glm", glmRowModel(glm), glm);
|
|
414
|
+
const copilotParts = renderProvider("copilot", copilotRowModel(copilot), copilot);
|
|
415
|
+
const fragment = doc.createDocumentFragment();
|
|
416
|
+
fragment.append(lastUpdated);
|
|
417
|
+
if (glmParts) fragment.append(...glmParts);
|
|
418
|
+
if (copilotParts) fragment.append(...copilotParts);
|
|
419
|
+
body.replaceChildren(fragment);
|
|
420
|
+
container.hidden = glmParts === null && copilotParts === null;
|
|
421
|
+
const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : "", locale);
|
|
422
|
+
lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading;
|
|
315
423
|
};
|
|
316
424
|
const render = (next) => {
|
|
317
425
|
snapshot = next;
|
|
318
|
-
|
|
319
|
-
const copilot = next?.providers?.find((provider) => provider.key === "copilot");
|
|
320
|
-
if (glm) renderGlm(doc, glmSection, glm, copy, locale);
|
|
321
|
-
if (copilot) renderCopilot(doc, copilotSection, copilot, copy, locale);
|
|
322
|
-
const updated = formatTime(next?.updatedAt ? new Date(next.updatedAt).toISOString() : "", locale);
|
|
323
|
-
lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading;
|
|
426
|
+
renderAll();
|
|
324
427
|
};
|
|
325
428
|
const showTransportError = () => {
|
|
326
429
|
if (snapshot) lastUpdated.textContent = copy.requestFailed;
|
|
@@ -360,7 +463,6 @@ function mountQuotaCard({
|
|
|
360
463
|
const observer = new win.MutationObserver(schedulePlace);
|
|
361
464
|
observer.observe(doc.body, { childList: true, subtree: true });
|
|
362
465
|
place();
|
|
363
|
-
applyCollapsed();
|
|
364
466
|
const startPolling = () => {
|
|
365
467
|
if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
|
|
366
468
|
void poll(false);
|
|
@@ -379,13 +481,25 @@ function mountQuotaCard({
|
|
|
379
481
|
const onRefresh = () => {
|
|
380
482
|
void poll(true);
|
|
381
483
|
};
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
484
|
+
const toggleRow = (key) => {
|
|
485
|
+
openKey = openKey === key ? void 0 : key;
|
|
486
|
+
renderAll();
|
|
487
|
+
};
|
|
488
|
+
const onBodyClick = (event) => {
|
|
489
|
+
const row = event.target?.closest?.("[data-dsh-quota-watch-row][data-expandable]");
|
|
490
|
+
if (row) toggleRow(row.dataset.dshQuotaWatchRow);
|
|
491
|
+
};
|
|
492
|
+
const onBodyKeydown = (event) => {
|
|
493
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
494
|
+
const row = event.target?.closest?.("[data-dsh-quota-watch-row][data-expandable]");
|
|
495
|
+
if (row) {
|
|
496
|
+
event.preventDefault();
|
|
497
|
+
toggleRow(row.dataset.dshQuotaWatchRow);
|
|
498
|
+
}
|
|
386
499
|
};
|
|
387
500
|
refreshButton.addEventListener("click", onRefresh);
|
|
388
|
-
|
|
501
|
+
body.addEventListener("click", onBodyClick);
|
|
502
|
+
body.addEventListener("keydown", onBodyKeydown);
|
|
389
503
|
doc.addEventListener("visibilitychange", onVisibilityChange);
|
|
390
504
|
startPolling();
|
|
391
505
|
return () => {
|
|
@@ -394,7 +508,8 @@ function mountQuotaCard({
|
|
|
394
508
|
observer.disconnect();
|
|
395
509
|
doc.removeEventListener("visibilitychange", onVisibilityChange);
|
|
396
510
|
refreshButton.removeEventListener("click", onRefresh);
|
|
397
|
-
|
|
511
|
+
body.removeEventListener("click", onBodyClick);
|
|
512
|
+
body.removeEventListener("keydown", onBodyKeydown);
|
|
398
513
|
container.remove();
|
|
399
514
|
};
|
|
400
515
|
}
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -1,68 +1,90 @@
|
|
|
1
1
|
import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
|
|
2
|
+
import { remainingToUsed } from './core/adapters.mjs'
|
|
2
3
|
|
|
3
4
|
export const name = 'quota-watch-client'
|
|
4
5
|
export const inject = []
|
|
5
6
|
|
|
6
7
|
const CARD_SELECTOR = '[data-dsh-quota-watch-card]'
|
|
7
|
-
const COLLAPSE_KEY = 'dsh-quota-watch:collapsed'
|
|
8
8
|
const FETCH_TIMEOUT_MS = 15_000
|
|
9
9
|
|
|
10
10
|
const COPY = {
|
|
11
11
|
zh: {
|
|
12
12
|
title: '套餐监控',
|
|
13
13
|
refresh: '刷新',
|
|
14
|
-
collapse: '收起',
|
|
15
|
-
expand: '展开',
|
|
16
14
|
loading: '读取中',
|
|
17
15
|
updated: '更新',
|
|
18
|
-
notConfigured: '未配置',
|
|
19
|
-
noPlan: '暂无套餐窗口',
|
|
20
16
|
failed: '查询失败',
|
|
21
17
|
stale: '数据可能已过期',
|
|
22
18
|
requestFailed: '刷新失败,显示上次成功数据',
|
|
23
19
|
available: '可用额度',
|
|
24
20
|
used: '本期已用',
|
|
25
|
-
balanceUnavailable: '余额暂不可用',
|
|
26
21
|
reset: '重置',
|
|
27
|
-
|
|
22
|
+
todayTokens: '今日 tokens',
|
|
23
|
+
todayCalls: '今日调用',
|
|
24
|
+
windows: {
|
|
25
|
+
'5h': '5 小时窗口 · 已用',
|
|
26
|
+
week: '每周窗口 · 已用',
|
|
27
|
+
month: '每月窗口 · 已用',
|
|
28
|
+
mcp: 'MCP(月)· 已用',
|
|
29
|
+
},
|
|
28
30
|
},
|
|
29
31
|
en: {
|
|
30
32
|
title: 'Quota Watch',
|
|
31
33
|
refresh: 'Refresh',
|
|
32
|
-
collapse: 'Collapse',
|
|
33
|
-
expand: 'Expand',
|
|
34
34
|
loading: 'Loading',
|
|
35
35
|
updated: 'Updated',
|
|
36
|
-
notConfigured: 'Not configured',
|
|
37
|
-
noPlan: 'No plan windows',
|
|
38
36
|
failed: 'Query failed',
|
|
39
37
|
stale: 'Data may be stale',
|
|
40
38
|
requestFailed: 'Refresh failed; showing last successful data',
|
|
41
39
|
available: 'Available credits',
|
|
42
40
|
used: 'Used this cycle',
|
|
43
|
-
balanceUnavailable: 'Balance unavailable',
|
|
44
41
|
reset: 'Resets',
|
|
45
|
-
|
|
42
|
+
todayTokens: 'Today tokens',
|
|
43
|
+
todayCalls: 'Calls today',
|
|
44
|
+
windows: {
|
|
45
|
+
'5h': '5-hour window · used',
|
|
46
|
+
week: 'Weekly window · used',
|
|
47
|
+
month: 'Monthly window · used',
|
|
48
|
+
mcp: 'MCP (month) · used',
|
|
49
|
+
},
|
|
46
50
|
},
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
const STYLE_TEXT = `
|
|
50
54
|
[data-dsh-quota-watch-card] { box-sizing: border-box; width: 100%; margin: 0 0 8px; color: var(--dsw-alias-label-primary, inherit); font: inherit; }
|
|
51
55
|
[data-dsh-quota-watch-card] *, [data-dsh-quota-watch-card] *::before, [data-dsh-quota-watch-card] *::after { box-sizing: border-box; }
|
|
52
|
-
.dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap:
|
|
56
|
+
.dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
53
57
|
.dqw-head { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
54
58
|
.dqw-title { flex: 1; min-width: 0; margin: 0; font-size: 12px; font-weight: 600; line-height: 18px; }
|
|
55
59
|
.dqw-button { border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: 11px; line-height: 18px; padding: 1px 4px; }
|
|
56
60
|
.dqw-button:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.16)); }
|
|
57
61
|
.dqw-button:disabled { cursor: wait; opacity: .6; }
|
|
58
62
|
.dqw-meta { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
59
|
-
.dqw-
|
|
60
|
-
.dqw-
|
|
63
|
+
.dqw-body { display: flex; flex-direction: column; }
|
|
64
|
+
.dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 7px 6px; margin: 1px -2px 2px; border-radius: 8px; }
|
|
65
|
+
.dqw-row-summary[data-expandable] { cursor: pointer; }
|
|
66
|
+
.dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
|
|
67
|
+
.dqw-row-summary:focus-visible { outline: 1px solid var(--dsw-alias-button-primary-fill, #5b8def); outline-offset: -1px; }
|
|
68
|
+
.dqw-row-summary.is-error { cursor: default; }
|
|
69
|
+
.dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
|
|
70
|
+
.dqw-bar { display: block; flex: 1; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
71
|
+
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
72
|
+
.dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
|
|
73
|
+
.dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
|
|
74
|
+
.dqw-pct { font-size: 10.5px; line-height: 14px; font-variant-numeric: tabular-nums; width: 30px; text-align: right; flex: none; }
|
|
75
|
+
.dqw-extra { font-size: 10px; line-height: 14px; opacity: .72; font-variant-numeric: tabular-nums; flex: none; }
|
|
76
|
+
.dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
|
|
77
|
+
.dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
|
|
78
|
+
.dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
79
|
+
.dqw-detail { padding: 2px 6px 6px; display: flex; flex-direction: column; gap: 7px; }
|
|
80
|
+
.dqw-bignums { display: flex; gap: 6px; }
|
|
81
|
+
.dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 6px 8px; }
|
|
82
|
+
.dqw-big .dqw-big-value { font-size: 16px; font-weight: 650; line-height: 20px; font-variant-numeric: tabular-nums; }
|
|
83
|
+
.dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
|
|
61
84
|
.dqw-row { display: flex; flex-direction: column; gap: 2px; }
|
|
62
85
|
.dqw-row-head { display: flex; justify-content: space-between; gap: 5px; font-size: 10px; line-height: 14px; }
|
|
63
|
-
.dqw-bar { display: block; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
64
|
-
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
65
86
|
.dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
87
|
+
.dqw-models { margin: 0; font-size: 10px; line-height: 15px; opacity: .8; border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; }
|
|
66
88
|
.dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
|
|
67
89
|
.dqw-credit { margin: 0; font-size: 10px; line-height: 14px; font-variant-numeric: tabular-nums; }
|
|
68
90
|
`
|
|
@@ -76,6 +98,14 @@ function formatNumber(value, locale) {
|
|
|
76
98
|
return new Intl.NumberFormat(locale === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 1 }).format(value)
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
function formatCompact(value, locale) {
|
|
102
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
103
|
+
return new Intl.NumberFormat(locale === 'zh' ? 'zh-CN' : 'en-US', {
|
|
104
|
+
notation: 'compact',
|
|
105
|
+
maximumFractionDigits: 1,
|
|
106
|
+
}).format(value)
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
function formatPercent(value, locale) {
|
|
80
110
|
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
81
111
|
const clamped = Math.max(0, Math.min(100, value))
|
|
@@ -113,102 +143,55 @@ function progress(doc, label, percent, locale) {
|
|
|
113
143
|
bar.setAttribute('aria-valuemax', '100')
|
|
114
144
|
bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, percent))))
|
|
115
145
|
const fill = doc.createElement('span')
|
|
116
|
-
fill.className =
|
|
146
|
+
fill.className = `dqw-bar-fill${percent > 95 ? ' danger' : percent > 80 ? ' warn' : ''}`
|
|
117
147
|
fill.style.width = `${Math.max(0, Math.min(100, percent))}%`
|
|
118
148
|
bar.append(fill)
|
|
119
149
|
row.append(head, bar)
|
|
120
150
|
return row
|
|
121
151
|
}
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
function renderGlm(doc, section, provider, copy, locale) {
|
|
141
|
-
const content = section.querySelector('[data-role="content"]')
|
|
142
|
-
content.replaceChildren()
|
|
143
|
-
appendStaleError(doc, content, provider, copy)
|
|
144
|
-
if (provider.status === 'missing') {
|
|
145
|
-
content.append(text(doc, 'p', 'dqw-muted', copy.notConfigured))
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
if (provider.status === 'loading') {
|
|
149
|
-
content.append(text(doc, 'p', 'dqw-muted', copy.loading))
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
if (!provider.plan?.windows?.length) {
|
|
153
|
-
if (provider.status !== 'error') content.append(text(doc, 'p', 'dqw-muted', copy.noPlan))
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
const planName = provider.plan.planName
|
|
157
|
-
if (planName) content.append(text(doc, 'p', 'dqw-muted', planName))
|
|
158
|
-
for (const window of provider.plan.windows) {
|
|
159
|
-
const label = window.name || copy.windows[window.key] || window.key
|
|
160
|
-
if (typeof window.percent === 'number' && Number.isFinite(window.percent)) {
|
|
161
|
-
content.append(progress(doc, label, window.percent, locale))
|
|
162
|
-
} else {
|
|
163
|
-
content.append(text(doc, 'p', 'dqw-muted', label))
|
|
164
|
-
}
|
|
165
|
-
const reset = formatTime(window.resetsAt, locale)
|
|
166
|
-
if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
153
|
+
/** GLM summary row view model: fixed 5h-window percent plus today's compact tokens. */
|
|
154
|
+
function glmRowModel(provider) {
|
|
155
|
+
if (provider?.status === 'missing') return null
|
|
156
|
+
if (provider?.status === 'error') return { kind: 'error', label: 'GLM', error: provider.error }
|
|
157
|
+
const windows = provider?.plan?.windows
|
|
158
|
+
if (!Array.isArray(windows) || windows.length === 0) return null
|
|
159
|
+
const five = windows.find((window) => window?.key === '5h')
|
|
160
|
+
const tokens = provider?.usage?.status === 'ready' || provider?.usage?.status === 'stale'
|
|
161
|
+
? provider?.usage?.totalTokens
|
|
162
|
+
: undefined
|
|
163
|
+
return {
|
|
164
|
+
kind: 'data',
|
|
165
|
+
label: 'GLM',
|
|
166
|
+
percent: five?.percent,
|
|
167
|
+
stale: provider.status === 'stale',
|
|
168
|
+
error: provider.error,
|
|
169
|
+
extra: tokens,
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
/** Copilot summary row view model: used% inverted from the reported remaining percent. */
|
|
174
|
+
function copilotRowModel(provider) {
|
|
175
|
+
if (provider?.status === 'missing') return null
|
|
176
|
+
if (provider?.status === 'error') return { kind: 'error', label: 'Copilot', error: provider.error }
|
|
177
|
+
const quota = provider?.quota
|
|
178
|
+
if (quota?.balanceAvailable !== true) return null
|
|
179
|
+
const used = remainingToUsed(quota.percentRemaining)
|
|
180
|
+
if (used === undefined) return null
|
|
181
|
+
return {
|
|
182
|
+
kind: 'data',
|
|
183
|
+
label: 'Copilot',
|
|
184
|
+
percent: used,
|
|
185
|
+
stale: provider.status === 'stale',
|
|
186
|
+
error: provider.error,
|
|
181
187
|
}
|
|
182
|
-
const quota = provider.quota
|
|
183
|
-
if (quota?.planName) content.append(text(doc, 'p', 'dqw-muted', quota.planName))
|
|
184
|
-
if (quota?.balanceAvailable === true) {
|
|
185
|
-
content.append(text(doc, 'p', 'dqw-credit', `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`))
|
|
186
|
-
if (typeof quota.percentRemaining === 'number') {
|
|
187
|
-
content.append(progress(doc, copy.available, quota.percentRemaining, locale))
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
content.append(text(doc, 'p', 'dqw-muted', copy.balanceUnavailable))
|
|
191
|
-
}
|
|
192
|
-
if (typeof quota?.creditsUsed === 'number') {
|
|
193
|
-
content.append(text(doc, 'p', 'dqw-muted', `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`))
|
|
194
|
-
}
|
|
195
|
-
const reset = formatTime(quota?.resetsAt, locale)
|
|
196
|
-
if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
197
188
|
}
|
|
198
189
|
|
|
199
190
|
function footArea(doc) {
|
|
200
|
-
const sidebar = doc.querySelector('[
|
|
191
|
+
const sidebar = doc.querySelector('[class*="sidebarCol"]')
|
|
201
192
|
return sidebar?.querySelector('[class*="footArea"]') ?? undefined
|
|
202
193
|
}
|
|
203
194
|
|
|
204
|
-
function readCollapsed(win) {
|
|
205
|
-
try { return win.localStorage.getItem(COLLAPSE_KEY) === '1' } catch { return false }
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function writeCollapsed(win, value) {
|
|
209
|
-
try { win.localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0') } catch { /* Storage may be unavailable. */ }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
195
|
async function fetchJson(fetchImpl, path, method) {
|
|
213
196
|
const response = await fetchImpl(path, {
|
|
214
197
|
...(method === 'POST' ? { method: 'POST' } : {}),
|
|
@@ -247,42 +230,146 @@ export function mountQuotaCard({
|
|
|
247
230
|
refreshButton.title = copy.refresh
|
|
248
231
|
refreshButton.setAttribute('aria-label', copy.refresh)
|
|
249
232
|
refreshButton.dataset.action = 'refresh'
|
|
250
|
-
|
|
251
|
-
collapseButton.type = 'button'
|
|
252
|
-
collapseButton.dataset.action = 'collapse'
|
|
233
|
+
header.append(refreshButton)
|
|
253
234
|
const body = doc.createElement('div')
|
|
254
235
|
body.className = 'dqw-body'
|
|
255
236
|
const lastUpdated = text(doc, 'p', 'dqw-meta', copy.loading)
|
|
256
237
|
lastUpdated.dataset.role = 'updated'
|
|
257
|
-
|
|
258
|
-
const copilotSection = providerSection(doc, 'copilot', 'GitHub Copilot')
|
|
259
|
-
body.append(lastUpdated, glmSection, copilotSection)
|
|
260
|
-
header.append(refreshButton, collapseButton)
|
|
238
|
+
body.append(lastUpdated)
|
|
261
239
|
card.append(header, body)
|
|
262
240
|
container.append(style, card)
|
|
263
|
-
let collapsed = readCollapsed(win)
|
|
264
241
|
let snapshot
|
|
265
242
|
let requestSequence = 0
|
|
266
243
|
let timer
|
|
267
244
|
let disposed = false
|
|
268
245
|
let queuedPlace = false
|
|
246
|
+
let openKey
|
|
247
|
+
|
|
248
|
+
const fillClass = (percent) => (percent > 95 ? 'dqw-bar-fill danger' : percent > 80 ? 'dqw-bar-fill warn' : 'dqw-bar-fill')
|
|
249
|
+
|
|
250
|
+
/** Build [row, detail] fragments for one provider, or null when it must not render. */
|
|
251
|
+
const renderProvider = (key, model, provider) => {
|
|
252
|
+
if (model === null) return null
|
|
253
|
+
const row = doc.createElement('div')
|
|
254
|
+
row.className = 'dqw-row-summary'
|
|
255
|
+
row.dataset.dshQuotaWatchRow = key
|
|
256
|
+
row.append(text(doc, 'span', 'dqw-label', model.label))
|
|
257
|
+
if (model.kind === 'error') {
|
|
258
|
+
row.classList.add('is-error')
|
|
259
|
+
row.append(text(doc, 'span', 'dqw-errtext', model.error ? `${copy.failed}: ${model.error}` : copy.failed))
|
|
260
|
+
return [row]
|
|
261
|
+
}
|
|
262
|
+
row.setAttribute('role', 'button')
|
|
263
|
+
row.tabIndex = 0
|
|
264
|
+
row.setAttribute('aria-expanded', String(openKey === key))
|
|
265
|
+
row.dataset.expandable = ''
|
|
266
|
+
if (typeof model.percent === 'number' && Number.isFinite(model.percent)) {
|
|
267
|
+
const bar = doc.createElement('span')
|
|
268
|
+
bar.className = 'dqw-bar'
|
|
269
|
+
bar.setAttribute('role', 'progressbar')
|
|
270
|
+
bar.setAttribute('aria-valuemin', '0')
|
|
271
|
+
bar.setAttribute('aria-valuemax', '100')
|
|
272
|
+
bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, model.percent))))
|
|
273
|
+
const fill = text(doc, 'span', fillClass(model.percent))
|
|
274
|
+
fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`
|
|
275
|
+
bar.append(fill)
|
|
276
|
+
row.append(bar)
|
|
277
|
+
}
|
|
278
|
+
if (model.stale) {
|
|
279
|
+
const mark = text(doc, 'span', 'dqw-stale-mark', '⚠')
|
|
280
|
+
mark.dataset.dshQuotaWatchStale = ''
|
|
281
|
+
mark.title = model.error ?? copy.stale
|
|
282
|
+
row.append(mark)
|
|
283
|
+
}
|
|
284
|
+
row.append(text(doc, 'span', 'dqw-pct', typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'))
|
|
285
|
+
if (model.extra !== undefined) {
|
|
286
|
+
const extra = text(doc, 'span', 'dqw-extra', formatCompact(model.extra, locale))
|
|
287
|
+
extra.dataset.dshQuotaWatchExtra = ''
|
|
288
|
+
row.append(extra)
|
|
289
|
+
}
|
|
290
|
+
row.append(text(doc, 'span', 'dqw-chev', openKey === key ? '▾' : '▸'))
|
|
291
|
+
const detail = doc.createElement('div')
|
|
292
|
+
detail.className = 'dqw-detail'
|
|
293
|
+
detail.dataset.dshQuotaWatchDetail = key
|
|
294
|
+
if (openKey === key) renderDetail(detail, key, provider)
|
|
295
|
+
return [row, detail]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderDetail(detail, key, provider) {
|
|
299
|
+
detail.replaceChildren()
|
|
300
|
+
if (provider?.status === 'stale') {
|
|
301
|
+
detail.append(text(doc, 'p', 'dqw-error', `${copy.stale}${provider.error ? `: ${provider.error}` : ''}`))
|
|
302
|
+
}
|
|
303
|
+
if (key === 'glm') {
|
|
304
|
+
const usage = provider?.usage
|
|
305
|
+
const numbers = doc.createElement('div')
|
|
306
|
+
numbers.className = 'dqw-bignums'
|
|
307
|
+
const tokensBig = doc.createElement('div')
|
|
308
|
+
tokensBig.className = 'dqw-big'
|
|
309
|
+
tokensBig.append(
|
|
310
|
+
text(doc, 'div', 'dqw-big-value', formatCompact(usage?.totalTokens, locale)),
|
|
311
|
+
text(doc, 'div', 'dqw-big-label', copy.todayTokens),
|
|
312
|
+
)
|
|
313
|
+
const callsBig = doc.createElement('div')
|
|
314
|
+
callsBig.className = 'dqw-big'
|
|
315
|
+
callsBig.append(
|
|
316
|
+
text(doc, 'div', 'dqw-big-value', formatNumber(usage?.totalCalls, locale)),
|
|
317
|
+
text(doc, 'div', 'dqw-big-label', copy.todayCalls),
|
|
318
|
+
)
|
|
319
|
+
numbers.append(tokensBig, callsBig)
|
|
320
|
+
detail.append(numbers)
|
|
321
|
+
for (const window of provider?.plan?.windows ?? []) {
|
|
322
|
+
const label = copy.windows[window?.key] ?? window?.key
|
|
323
|
+
if (typeof window?.percent === 'number' && Number.isFinite(window.percent)) {
|
|
324
|
+
detail.append(progress(doc, label, window.percent, locale))
|
|
325
|
+
} else {
|
|
326
|
+
detail.append(text(doc, 'p', 'dqw-muted', label))
|
|
327
|
+
}
|
|
328
|
+
const reset = formatTime(window?.resetsAt, locale)
|
|
329
|
+
if (reset) detail.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
330
|
+
}
|
|
331
|
+
const models = Array.isArray(usage?.models) && usage.models.length > 0
|
|
332
|
+
? usage.models.map((model) => `${model.name} ${formatCompact(model.tokens, locale)}`).join(' · ')
|
|
333
|
+
: undefined
|
|
334
|
+
if (models) detail.append(text(doc, 'p', 'dqw-models', models))
|
|
335
|
+
const updated = formatTime(provider?.updatedAt ? new Date(provider.updatedAt).toISOString() : '', locale)
|
|
336
|
+
if (updated) detail.append(text(doc, 'p', 'dqw-meta', `${copy.updated}: ${updated}`))
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
const quota = provider?.quota
|
|
340
|
+
if (quota?.balanceAvailable === true) {
|
|
341
|
+
detail.append(text(doc, 'p', 'dqw-credit', `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`))
|
|
342
|
+
const used = remainingToUsed(quota.percentRemaining)
|
|
343
|
+
if (used !== undefined) detail.append(progress(doc, copy.used, used, locale))
|
|
344
|
+
if (typeof quota.creditsUsed === 'number' && quota.creditsUsed >= 0) {
|
|
345
|
+
detail.append(text(doc, 'p', 'dqw-muted', `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`))
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
detail.append(text(doc, 'p', 'dqw-muted', copy.stale))
|
|
349
|
+
}
|
|
350
|
+
const reset = formatTime(quota?.resetsAt, locale)
|
|
351
|
+
if (reset) detail.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
352
|
+
}
|
|
269
353
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
354
|
+
const renderAll = () => {
|
|
355
|
+
const providers = snapshot?.providers ?? []
|
|
356
|
+
const glm = providers.find((provider) => provider?.key === 'glm')
|
|
357
|
+
const copilot = providers.find((provider) => provider?.key === 'copilot')
|
|
358
|
+
const glmParts = renderProvider('glm', glmRowModel(glm), glm)
|
|
359
|
+
const copilotParts = renderProvider('copilot', copilotRowModel(copilot), copilot)
|
|
360
|
+
const fragment = doc.createDocumentFragment()
|
|
361
|
+
fragment.append(lastUpdated)
|
|
362
|
+
if (glmParts) fragment.append(...glmParts)
|
|
363
|
+
if (copilotParts) fragment.append(...copilotParts)
|
|
364
|
+
body.replaceChildren(fragment)
|
|
365
|
+
container.hidden = glmParts === null && copilotParts === null
|
|
366
|
+
const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '', locale)
|
|
367
|
+
lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading
|
|
276
368
|
}
|
|
277
369
|
|
|
278
370
|
const render = (next) => {
|
|
279
371
|
snapshot = next
|
|
280
|
-
|
|
281
|
-
const copilot = next?.providers?.find((provider) => provider.key === 'copilot')
|
|
282
|
-
if (glm) renderGlm(doc, glmSection, glm, copy, locale)
|
|
283
|
-
if (copilot) renderCopilot(doc, copilotSection, copilot, copy, locale)
|
|
284
|
-
const updated = formatTime(next?.updatedAt ? new Date(next.updatedAt).toISOString() : '', locale)
|
|
285
|
-
lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading
|
|
372
|
+
renderAll()
|
|
286
373
|
}
|
|
287
374
|
|
|
288
375
|
const showTransportError = () => {
|
|
@@ -327,7 +414,6 @@ export function mountQuotaCard({
|
|
|
327
414
|
const observer = new win.MutationObserver(schedulePlace)
|
|
328
415
|
observer.observe(doc.body, { childList: true, subtree: true })
|
|
329
416
|
place()
|
|
330
|
-
applyCollapsed()
|
|
331
417
|
|
|
332
418
|
const startPolling = () => {
|
|
333
419
|
if (timer !== undefined || doc.visibilityState === 'hidden' || disposed) return
|
|
@@ -343,13 +429,25 @@ export function mountQuotaCard({
|
|
|
343
429
|
else startPolling()
|
|
344
430
|
}
|
|
345
431
|
const onRefresh = () => { void poll(true) }
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
432
|
+
const toggleRow = (key) => {
|
|
433
|
+
openKey = openKey === key ? undefined : key
|
|
434
|
+
renderAll()
|
|
435
|
+
}
|
|
436
|
+
const onBodyClick = (event) => {
|
|
437
|
+
const row = event.target?.closest?.('[data-dsh-quota-watch-row][data-expandable]')
|
|
438
|
+
if (row) toggleRow(row.dataset.dshQuotaWatchRow)
|
|
439
|
+
}
|
|
440
|
+
const onBodyKeydown = (event) => {
|
|
441
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
442
|
+
const row = event.target?.closest?.('[data-dsh-quota-watch-row][data-expandable]')
|
|
443
|
+
if (row) {
|
|
444
|
+
event.preventDefault()
|
|
445
|
+
toggleRow(row.dataset.dshQuotaWatchRow)
|
|
446
|
+
}
|
|
350
447
|
}
|
|
351
448
|
refreshButton.addEventListener('click', onRefresh)
|
|
352
|
-
|
|
449
|
+
body.addEventListener('click', onBodyClick)
|
|
450
|
+
body.addEventListener('keydown', onBodyKeydown)
|
|
353
451
|
doc.addEventListener('visibilitychange', onVisibilityChange)
|
|
354
452
|
startPolling()
|
|
355
453
|
|
|
@@ -359,7 +457,8 @@ export function mountQuotaCard({
|
|
|
359
457
|
observer.disconnect()
|
|
360
458
|
doc.removeEventListener('visibilitychange', onVisibilityChange)
|
|
361
459
|
refreshButton.removeEventListener('click', onRefresh)
|
|
362
|
-
|
|
460
|
+
body.removeEventListener('click', onBodyClick)
|
|
461
|
+
body.removeEventListener('keydown', onBodyKeydown)
|
|
363
462
|
container.remove()
|
|
364
463
|
}
|
|
365
464
|
}
|
package/src/core/adapters.mjs
CHANGED
|
@@ -49,7 +49,7 @@ function toIso(value) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function glmPercent(row, kind) {
|
|
52
|
-
if (kind === 'CREDIT_LIMIT') {
|
|
52
|
+
if (kind === 'CREDIT_LIMIT' || kind === 'TIME_LIMIT') {
|
|
53
53
|
const current = asNumber(row.currentValue)
|
|
54
54
|
const total = asNumber(row.usage)
|
|
55
55
|
if (current !== undefined && total !== undefined && total > 0) {
|
|
@@ -74,6 +74,45 @@ export function buildGlmProbe({ provider, apiKey } = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
const GLM_USAGE_PATH = '/api/monitor/usage/model-usage'
|
|
78
|
+
|
|
79
|
+
/** Build a GLM model-usage request over a formatted local-time interval. */
|
|
80
|
+
export function buildGlmUsageProbe({ provider, apiKey, startTime, endTime } = {}) {
|
|
81
|
+
const host = GLM_HOSTS[provider]
|
|
82
|
+
const key = asString(apiKey)
|
|
83
|
+
const from = asString(startTime)
|
|
84
|
+
const to = asString(endTime)
|
|
85
|
+
if (host === undefined || key === undefined || from === undefined || to === undefined) return undefined
|
|
86
|
+
return {
|
|
87
|
+
url: `https://${host}${GLM_USAGE_PATH}?startTime=${encodeURIComponent(from)}&endTime=${encodeURIComponent(to)}`,
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: key,
|
|
90
|
+
'accept-language': 'en-US,en',
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Format a Date as the provider's local-time `yyyy-MM-dd HH:mm:ss` interval stamp. */
|
|
96
|
+
export function formatGlmIntervalTime(date) {
|
|
97
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return undefined
|
|
98
|
+
const pad = (value) => String(value).padStart(2, '0')
|
|
99
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The host-local calendar day so far, as a model-usage query interval. */
|
|
103
|
+
export function usageDayWindow(now = new Date()) {
|
|
104
|
+
const end = formatGlmIntervalTime(now)
|
|
105
|
+
if (end === undefined) return undefined
|
|
106
|
+
return { startTime: `${end.slice(0, 10)} 00:00:00`, endTime: end }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Normalize a provider-reported remaining percentage to used, without other fields. */
|
|
110
|
+
export function remainingToUsed(percentRemaining) {
|
|
111
|
+
const value = asNumber(percentRemaining)
|
|
112
|
+
if (value === undefined || value < 0 || value > 100) return undefined
|
|
113
|
+
return clampPercent(Math.round((100 - value) * 100) / 100)
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
/** Normalize GLM's provider-defined quota windows without relying on row order. */
|
|
78
117
|
export function parseGlmPlan(status, body) {
|
|
79
118
|
if (status !== 200) return undefined
|
|
@@ -87,13 +126,20 @@ export function parseGlmPlan(status, body) {
|
|
|
87
126
|
const row = asRecord(candidate)
|
|
88
127
|
if (row === undefined) continue
|
|
89
128
|
const kind = asString(row.type)
|
|
90
|
-
if (kind
|
|
91
|
-
|
|
129
|
+
if (kind !== 'TOKENS_LIMIT' && kind !== 'CREDIT_LIMIT' && kind !== 'TIME_LIMIT') continue
|
|
130
|
+
const percent = glmPercent(row, kind)
|
|
131
|
+
const resetsAt = toIso(row.nextResetTime)
|
|
132
|
+
if (kind === 'TIME_LIMIT') {
|
|
133
|
+
windows.push({
|
|
134
|
+
key: 'mcp',
|
|
135
|
+
...(percent !== undefined ? { percent } : {}),
|
|
136
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
137
|
+
})
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
92
140
|
const unit = asNumber(row.unit)
|
|
93
141
|
const key = GLM_WINDOW_KEYS[unit]
|
|
94
142
|
if (key === undefined) continue
|
|
95
|
-
const percent = glmPercent(row, kind)
|
|
96
|
-
const resetsAt = toIso(row.nextResetTime)
|
|
97
143
|
windows.push({
|
|
98
144
|
key,
|
|
99
145
|
...(percent !== undefined ? { percent } : {}),
|
|
@@ -106,6 +152,29 @@ export function parseGlmPlan(status, body) {
|
|
|
106
152
|
return { ...(planName !== undefined ? { planName } : {}), windows }
|
|
107
153
|
}
|
|
108
154
|
|
|
155
|
+
/** Normalize GLM's provider-reported usage totals for a queried interval. */
|
|
156
|
+
export function parseGlmUsage(status, body) {
|
|
157
|
+
if (status !== 200) return undefined
|
|
158
|
+
const root = asRecord(body)
|
|
159
|
+
if (root?.success !== true) return undefined
|
|
160
|
+
const data = asRecord(root.data)
|
|
161
|
+
const total = asRecord(data?.totalUsage)
|
|
162
|
+
const totalTokens = asNumber(total?.totalTokensUsage)
|
|
163
|
+
const totalCalls = asNumber(total?.totalModelCallCount)
|
|
164
|
+
if (totalTokens === undefined || totalTokens < 0) return undefined
|
|
165
|
+
if (totalCalls === undefined || totalCalls < 0) return undefined
|
|
166
|
+
const list = Array.isArray(total.modelSummaryList) ? total.modelSummaryList : []
|
|
167
|
+
const models = []
|
|
168
|
+
for (const candidate of list) {
|
|
169
|
+
const row = asRecord(candidate)
|
|
170
|
+
const name = asString(row?.modelName)
|
|
171
|
+
const tokens = asNumber(row?.totalTokens)
|
|
172
|
+
if (name === undefined || tokens === undefined || tokens < 0) continue
|
|
173
|
+
models.push({ name, tokens })
|
|
174
|
+
}
|
|
175
|
+
return { totalTokens, totalCalls, ...(models.length > 0 ? { models } : {}) }
|
|
176
|
+
}
|
|
177
|
+
|
|
109
178
|
/** Build the Copilot user-quota request using the raw GitHub OAuth grant. */
|
|
110
179
|
export function buildCopilotProbe({ githubOAuthToken } = {}) {
|
|
111
180
|
const token = asString(githubOAuthToken)
|
package/src/host.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { credentialKey, credentialRef, isCredentialRefName } from '@deepseek-ai/dsh-credentials'
|
|
2
|
-
import { buildCopilotProbe, buildGlmProbe, parseCopilotUser, parseGlmPlan } from './core/adapters.mjs'
|
|
2
|
+
import { buildCopilotProbe, buildGlmProbe, buildGlmUsageProbe, parseCopilotUser, parseGlmPlan, parseGlmUsage, usageDayWindow } from './core/adapters.mjs'
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_POLL_INTERVAL_SEC,
|
|
5
5
|
MAX_POLL_INTERVAL_SEC,
|
|
@@ -133,6 +133,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
133
133
|
...(provider.error !== undefined ? { error: provider.error } : {}),
|
|
134
134
|
...(provider.plan !== undefined ? { plan: provider.plan } : {}),
|
|
135
135
|
...(provider.quota !== undefined ? { quota: provider.quota } : {}),
|
|
136
|
+
...(provider.usage !== undefined ? { usage: provider.usage } : {}),
|
|
136
137
|
}
|
|
137
138
|
}),
|
|
138
139
|
}
|
|
@@ -161,6 +162,52 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
161
162
|
})
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
function markGlmPlanSuccess(plan) {
|
|
166
|
+
const previous = providers.get('glm')
|
|
167
|
+
const at = now()
|
|
168
|
+
providers.set('glm', {
|
|
169
|
+
key: 'glm',
|
|
170
|
+
displayName: 'GLM Coding Plan',
|
|
171
|
+
status: 'ready',
|
|
172
|
+
credential: 'configured',
|
|
173
|
+
updatedAt: at,
|
|
174
|
+
checkedAt: at,
|
|
175
|
+
plan,
|
|
176
|
+
...(previous?.usage !== undefined ? { usage: previous.usage } : {}),
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function markGlmUsageSuccess(usage) {
|
|
181
|
+
const previous = providers.get('glm')
|
|
182
|
+
if (previous === undefined || previous.key !== 'glm') return
|
|
183
|
+
const at = now()
|
|
184
|
+
providers.set('glm', {
|
|
185
|
+
...previous,
|
|
186
|
+
status: 'ready',
|
|
187
|
+
credential: 'configured',
|
|
188
|
+
updatedAt: at,
|
|
189
|
+
checkedAt: at,
|
|
190
|
+
usage: { status: 'ready', updatedAt: at, ...usage },
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function markGlmUsageFailure(error) {
|
|
195
|
+
const previous = providers.get('glm')
|
|
196
|
+
if (previous === undefined || previous.key !== 'glm') return
|
|
197
|
+
const lastGood = previous.usage?.status === 'ready' || previous.usage?.status === 'stale'
|
|
198
|
+
? previous.usage
|
|
199
|
+
: undefined
|
|
200
|
+
providers.set('glm', {
|
|
201
|
+
...previous,
|
|
202
|
+
checkedAt: now(),
|
|
203
|
+
usage: {
|
|
204
|
+
...(lastGood !== undefined ? lastGood : {}),
|
|
205
|
+
status: lastGood !== undefined ? 'stale' : 'error',
|
|
206
|
+
error: safeProbeError(error),
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
164
211
|
function markFailure(key, displayName, error) {
|
|
165
212
|
const previous = providers.get(key)
|
|
166
213
|
const checkedAt = now()
|
|
@@ -206,9 +253,26 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
206
253
|
const { status, body } = await fetchBody(spec)
|
|
207
254
|
const plan = parseGlmPlan(status, body)
|
|
208
255
|
if (plan === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
|
|
209
|
-
|
|
256
|
+
markGlmPlanSuccess(plan)
|
|
210
257
|
} catch (error) {
|
|
211
258
|
markFailure('glm', 'GLM Coding Plan', error)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
await probeGlmUsage(provider, apiKey)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function probeGlmUsage(provider, apiKey) {
|
|
265
|
+
const window = usageDayWindow(new Date(now()))
|
|
266
|
+
if (window === undefined) return
|
|
267
|
+
try {
|
|
268
|
+
const spec = buildGlmUsageProbe({ provider, apiKey, startTime: window.startTime, endTime: window.endTime })
|
|
269
|
+
if (spec === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
|
|
270
|
+
const { status, body } = await fetchBody(spec)
|
|
271
|
+
const usage = parseGlmUsage(status, body)
|
|
272
|
+
if (usage === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
|
|
273
|
+
markGlmUsageSuccess(usage)
|
|
274
|
+
} catch (error) {
|
|
275
|
+
markGlmUsageFailure(error)
|
|
212
276
|
}
|
|
213
277
|
}
|
|
214
278
|
|