dsh-subusage 0.10.10 → 0.10.11
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 +4 -4
- package/lib/client.js +219 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **English.** dsh-subusage shows subscription quota and balance for the AI providers you already use, as a pill next to the model selector in DeepSeek Harness. Provider switches and credentials live in one settings page, and other plugins or agents can read the same snapshot through a read-only quota API.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
10
|
[](#license--security--许可与安全)
|
|
11
11
|
[](#compatibility--兼容性)
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
|
|
19
19
|
| 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
药丸只画图形:每个额度窗口一根彩色余量条(绿 / 黄 / 橙 / 红,满 = 100% 剩余),正常时不写字;需要留意时追加一个词,如 `周 偏少`、`5时 已用尽`、`需配置`、`拉取失败`。点击弹层,顶部先给结论(额度充足 / 余量偏低 / 即将用尽 / 额度已用尽…),下面是各窗口的剩余百分比与重置倒计时;成功结果按提供商缓存 **60 秒**,手动刷新可跳过普通 TTL(仍遵守上游限流退避与 `Retry-After`)。
|
|
22
22
|
|
|
23
23
|
本插件**不注册模型路由**,药丸按 provider id 匹配;因此使用哪个插件提供该模型路由不影响匹配,id 对不上时才不显示。
|
|
24
24
|
|
|
@@ -385,13 +385,13 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
|
|
|
385
385
|
|
|
386
386
|
### 界面预览
|
|
387
387
|
|
|
388
|
-
以下为 0.
|
|
388
|
+
以下为 0.10.11 的离线组件测试截图,使用虚构用量数据,不包含真实账号信息,也不是运行中的 DSH 截图。
|
|
389
389
|
|
|
390
390
|
深色设置页:查看订阅用量、管理凭据与提供商。
|
|
391
391
|
|
|
392
392
|

|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
浅色用量弹层:顶部先给结论(额度充足 / 余量偏低 / 即将用尽…),下面是各窗口的剩余百分比与重置时间。右下角是收起态药丸——每个窗口一根彩条,正常时不写字。
|
|
395
395
|
|
|
396
396
|

|
|
397
397
|
|
package/lib/client.js
CHANGED
|
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
lightOk: "全部拉取成功",
|
|
32
32
|
lightFail: "存在拉取失败",
|
|
33
33
|
statusOk: "数据获取成功",
|
|
34
|
-
statusPartial: "
|
|
34
|
+
statusPartial: "数据不完整",
|
|
35
35
|
statusCache: "缓存数据",
|
|
36
36
|
cacheAge: "缓存 · {n} 分钟前",
|
|
37
37
|
cacheUnknown: "缓存时间未知",
|
|
@@ -91,16 +91,27 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
restDays: "{n} 天",
|
|
92
92
|
restHours: "{n} 小时",
|
|
93
93
|
restMins: "{n} 分钟",
|
|
94
|
-
|
|
95
|
-
badgeHigh
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// 档位词只描述「还剩多少」;旧的「偏高」没写清主语(用量高还是余量高),读起来自相矛盾。
|
|
95
|
+
// 键名 badgeHigh 保留(它对应已用 71–90% 的档位),文案改成「偏少」。
|
|
96
|
+
badgeOk: "充足",
|
|
97
|
+
badgeHigh: "偏少",
|
|
98
|
+
badgeTight: "即将用尽",
|
|
99
|
+
badgeLimited: "已用尽",
|
|
98
100
|
badgeBlocked: "受连累",
|
|
101
|
+
statusPlenty: "额度充足",
|
|
102
|
+
statusLow: "余量偏低",
|
|
103
|
+
statusTight: "即将用尽",
|
|
104
|
+
statusExhausted: "额度已用尽",
|
|
105
|
+
statusBlocked: "暂不可用",
|
|
106
|
+
statusWorst: "最紧张:{w} · 剩 {p}%",
|
|
107
|
+
statusOnly: "{w} · 剩 {p}%",
|
|
108
|
+
statusResetIn: "{rest}后重置",
|
|
109
|
+
remainWord: "剩余",
|
|
99
110
|
numUsed: "已用 {p}%",
|
|
100
111
|
numLimited: "⚠ 已达限额",
|
|
101
112
|
numBlocked: "⚠ 暂不可用",
|
|
102
113
|
pillRemaining: "余 {p}%",
|
|
103
|
-
pillLimited: "
|
|
114
|
+
pillLimited: "已用尽",
|
|
104
115
|
pillBlocked: "受连累",
|
|
105
116
|
needConfigShort: "需配置",
|
|
106
117
|
pillNeedKey: "未配置 API Key —— 请打开 设置 → 订阅用量 填写,或导出同名环境变量后重启。",
|
|
@@ -144,7 +155,7 @@ window.__ModuleLoader__.load({
|
|
|
144
155
|
lightOk: "All fetches succeeded",
|
|
145
156
|
lightFail: "Some fetches failed",
|
|
146
157
|
statusOk: "Fetch OK",
|
|
147
|
-
statusPartial: "Partial data
|
|
158
|
+
statusPartial: "Partial data",
|
|
148
159
|
statusCache: "Cached data",
|
|
149
160
|
cacheAge: "Cached · {n} min ago",
|
|
150
161
|
cacheUnknown: "Cache age unknown",
|
|
@@ -204,16 +215,26 @@ window.__ModuleLoader__.load({
|
|
|
204
215
|
restDays: "{n} d",
|
|
205
216
|
restHours: "{n} h",
|
|
206
217
|
restMins: "{n} min",
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
// Tier words describe what is LEFT; the old "High" never said high what.
|
|
219
|
+
badgeOk: "Plenty",
|
|
220
|
+
badgeHigh: "Low",
|
|
221
|
+
badgeTight: "Almost out",
|
|
222
|
+
badgeLimited: "Used up",
|
|
211
223
|
badgeBlocked: "Blocked",
|
|
224
|
+
statusPlenty: "Plenty left",
|
|
225
|
+
statusLow: "Running low",
|
|
226
|
+
statusTight: "Almost out",
|
|
227
|
+
statusExhausted: "Quota used up",
|
|
228
|
+
statusBlocked: "Temporarily blocked",
|
|
229
|
+
statusWorst: "Tightest: {w} · {p}% left",
|
|
230
|
+
statusOnly: "{w} · {p}% left",
|
|
231
|
+
statusResetIn: "resets in {rest}",
|
|
232
|
+
remainWord: "left",
|
|
212
233
|
numUsed: "Used {p}%",
|
|
213
234
|
numLimited: "⚠ Limit reached",
|
|
214
235
|
numBlocked: "⚠ Unavailable",
|
|
215
236
|
pillRemaining: "{p}% left",
|
|
216
|
-
pillLimited: "
|
|
237
|
+
pillLimited: "Used up",
|
|
217
238
|
pillBlocked: "Blocked",
|
|
218
239
|
needConfigShort: "Setup",
|
|
219
240
|
pillNeedKey: "No API key — open Settings → Subscription usage to fill one in, or export the same-named env variable and restart.",
|
|
@@ -920,13 +941,126 @@ window.__ModuleLoader__.load({
|
|
|
920
941
|
);
|
|
921
942
|
}
|
|
922
943
|
|
|
944
|
+
// ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
|
|
945
|
+
// 注意:下面这些都是**普通函数调用**而不是 React 组件元素——它们不用 Hook,直接内联展开,
|
|
946
|
+
// 测试里的 React 桩也能看到它们渲染出的文字。
|
|
947
|
+
const TRACK = "rgba(128,128,128,.22)";
|
|
948
|
+
const NEUTRAL = "#94a3b8";
|
|
949
|
+
const INFO = "#3b82f6";
|
|
950
|
+
/** #rrggbb → rgba(r,g,b,a):底色与描边都由状态色派生,保证同一状态到处同一个颜色。 */
|
|
951
|
+
function alpha(color, a) {
|
|
952
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color || "");
|
|
953
|
+
return m ? `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})` : `rgba(128,128,128,${a})`;
|
|
954
|
+
}
|
|
955
|
+
/** 状态色实心圆上的字形色:黄色配深字,其余白字。 */
|
|
956
|
+
function onColor(color) { return color === "#eab308" ? "#1f1300" : "#fff"; }
|
|
957
|
+
/** 状态色用作文字时的版本:纯黄在浅色主题上看不清,加深一档。 */
|
|
958
|
+
function textTone(color) { return color === "#eab308" ? "#ca8a04" : color; }
|
|
959
|
+
function windowLeft(w) { return w.status === "rate-limited" ? 0 : remainingPercent(w.percent); }
|
|
960
|
+
/** 状态色实心圆 + 字形(✓ ⚠ ✕ ? ! ⚙ ℹ …):颜色之外还有形状,不靠颜色单独传达。 */
|
|
961
|
+
function statusGlyph(color, icon, size) {
|
|
962
|
+
return h("span", { "aria-hidden": "true", style: { flex: "none", width: size, height: size, borderRadius: "50%", background: color, color: onColor(color), display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: Math.round(size * 0.62), fontWeight: 700, lineHeight: 1 } }, icon);
|
|
963
|
+
}
|
|
964
|
+
/** 小标签:状态色圆点 + 继承前景色的文字(文字不着色,深浅主题都清晰)。 */
|
|
965
|
+
function statusChip(text, color) {
|
|
966
|
+
return h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11, lineHeight: 1.5, padding: "1px 8px", borderRadius: 999, background: alpha(color, ".14"), whiteSpace: "nowrap" } },
|
|
967
|
+
h("span", { "aria-hidden": "true", style: { width: 6, height: 6, borderRadius: "50%", background: color } }), text);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* 药丸里的迷你余量条:一个窗口一根横条,满 = 100% 剩余,颜色是该窗口自己的档位色。
|
|
971
|
+
* 条的顺序与弹层里窗口行的顺序一致,所以不用文字也能一眼对上;窗口越多条越细(最多 5 根)。
|
|
972
|
+
*/
|
|
973
|
+
function miniBars(windows, muted) {
|
|
974
|
+
const list = windows.slice(0, 5), n = list.length;
|
|
975
|
+
const barH = n <= 1 ? 6 : n === 2 ? 5 : n === 3 ? 4 : n === 4 ? 3 : 2;
|
|
976
|
+
return h("span", { "aria-hidden": "true", style: { display: "inline-flex", flexDirection: "column", justifyContent: "center", gap: n <= 2 ? 3 : 2, width: 34, flex: "none", opacity: muted ? 0.5 : 1 } },
|
|
977
|
+
list.map((w, i) => {
|
|
978
|
+
const left = windowLeft(w);
|
|
979
|
+
return h("span", { key: `${w.kind}:${i}`, style: { display: "block", height: barH, borderRadius: 99, background: TRACK, overflow: "hidden" } },
|
|
980
|
+
h("span", { style: { display: "block", width: `${left}%`, minWidth: left > 0 ? 3 : 0, height: "100%", borderRadius: 99, background: usageTier(w).color } }));
|
|
981
|
+
}));
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* 订阅整体状态:一个对象回答「现在订阅怎么样」,药丸、弹层横幅、设置页横幅共用,
|
|
985
|
+
* 所以三处永远同一个结论、同一个颜色。优先级:额度用尽 > Cookie 到期 > 需配置 > 读取失败 > 缓存/不完整 > 额度档位。
|
|
986
|
+
* 返回 { key, color, icon, title, sub, short, tinted, ... }:
|
|
987
|
+
* title/sub 给弹层横幅;short 是药丸上的**一个词**(正常时为空——绿色余量条本身就是答案);
|
|
988
|
+
* tinted 为真时药丸整体染色,提醒「这里需要你看一眼」。
|
|
989
|
+
*/
|
|
990
|
+
function computeStatus(rawEntry, { failed = null, failState, providerId, t, getLocale }) {
|
|
991
|
+
const entry = canShowUsage(rawEntry) ? rawEntry : null;
|
|
992
|
+
const windows = entry ? entry.windows || [] : [];
|
|
993
|
+
const worst = windows.length ? worstWindow(windows) : null;
|
|
994
|
+
const worstTier = worst ? usageTier(worst) : null;
|
|
995
|
+
const multi = windows.length > 1;
|
|
996
|
+
const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
|
|
997
|
+
const state = rawEntry ? rawEntry.state : failState;
|
|
998
|
+
const expiry = cookieExpiry(rawEntry, t);
|
|
999
|
+
const expiring = !!(expiry && expiry.rank >= 2);
|
|
1000
|
+
// 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时主文案仍是失败原因;
|
|
1001
|
+
// 例外是已过期——它本身就是「请重新登录」的行动项。
|
|
1002
|
+
const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || expiry.expired);
|
|
1003
|
+
const windowName = (w) => t(KIND_LABEL[w.kind] ?? "wall");
|
|
1004
|
+
const windowShort = (w) => multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
|
|
1005
|
+
const windowSub = (w) => {
|
|
1006
|
+
const head = t(multi ? "statusWorst" : "statusOnly").replace("{w}", windowName(w)).replace("{p}", String(windowLeft(w)));
|
|
1007
|
+
const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
|
|
1008
|
+
return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
|
|
1009
|
+
};
|
|
1010
|
+
const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
|
|
1011
|
+
: state === "no-cookie" ? t("pillNeedCookie") : null;
|
|
1012
|
+
const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
|
|
1013
|
+
if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
|
|
1014
|
+
if (state === "disabled") return { ...base, key: "disabled", color: NEUTRAL, icon: "–", title: t("statusDisabled"), sub: "", short: t("statusDisabled"), tinted: false };
|
|
1015
|
+
if (worstTier && worstTier.rank >= 4) {
|
|
1016
|
+
const title = t(worst.cascade ? "statusBlocked" : "statusExhausted");
|
|
1017
|
+
return { ...base, key: "blocked", color: worstTier.color, icon: "✕", title, sub: windowSub(worst), short: windowShort(worst) + t(worst.cascade ? "pillBlocked" : "pillLimited"), tinted: true };
|
|
1018
|
+
}
|
|
1019
|
+
if (expiryActionable) {
|
|
1020
|
+
return { ...base, key: "cookie", color: expiry.color, icon: expiry.icon, title: expiry.label, sub: t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale())), short: expiry.label, tinted: true };
|
|
1021
|
+
}
|
|
1022
|
+
if (needConfig) {
|
|
1023
|
+
return { ...base, key: "config", color: INFO, icon: "⚙", title: t(state === "no-key" ? "statusNoKey" : "statusNoCookie"), sub: needConfig, short: t(state === "no-key" ? "needConfigShort" : "statusNoCookie"), tinted: true };
|
|
1024
|
+
}
|
|
1025
|
+
if (!entry) {
|
|
1026
|
+
return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: failed || (rawEntry && rawEntry.error) || "", short: t("statusError"), tinted: true };
|
|
1027
|
+
}
|
|
1028
|
+
if (degraded) {
|
|
1029
|
+
// 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
|
|
1030
|
+
const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
|
|
1031
|
+
const worse = worstTier && worstTier.rank >= 3;
|
|
1032
|
+
return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: label, tinted: true };
|
|
1033
|
+
}
|
|
1034
|
+
if (worstTier) {
|
|
1035
|
+
const sub = windowSub(worst);
|
|
1036
|
+
if (worstTier.rank >= 3) return { ...base, key: "tight", color: worstTier.color, icon: "⚠", title: t("statusTight"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
|
|
1037
|
+
if (worstTier.rank === 2) return { ...base, key: "low", color: worstTier.color, icon: "⚠", title: t("statusLow"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
|
|
1038
|
+
return { ...base, key: "ok", color: worstTier.color, icon: "✓", title: t("statusPlenty"), sub, short: "", tinted: false };
|
|
1039
|
+
}
|
|
1040
|
+
// 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
|
|
1041
|
+
const balance = balanceText(entry, t);
|
|
1042
|
+
return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
|
|
1043
|
+
}
|
|
1044
|
+
/** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
|
|
1045
|
+
function statusBanner(status) {
|
|
1046
|
+
return h("div", { role: "status", style: { display: "flex", alignItems: "center", gap: 12, marginTop: 10, padding: "12px 14px", borderRadius: 12, background: alpha(status.color, ".10"), border: `1px solid ${alpha(status.color, ".28")}` } },
|
|
1047
|
+
statusGlyph(status.color, status.icon, 30),
|
|
1048
|
+
h("div", { style: { minWidth: 0, flex: 1 } },
|
|
1049
|
+
h("div", { style: { fontSize: 15, fontWeight: 700, lineHeight: 1.3, overflowWrap: "anywhere" } }, status.title),
|
|
1050
|
+
status.sub ? h("div", { style: { fontSize: 11.5, opacity: 0.75, marginTop: 2, overflowWrap: "anywhere", whiteSpace: "pre-wrap" } }, status.sub) : null));
|
|
1051
|
+
}
|
|
1052
|
+
/** 套餐名(extras 里 kind=plan):放在标题行右侧做成小标签,不再混在余额明细里。 */
|
|
1053
|
+
function planName(entry) {
|
|
1054
|
+
const row = entry && Array.isArray(entry.extras) ? entry.extras.find((x) => x.kind === "plan") : undefined;
|
|
1055
|
+
return row ? row.value : null;
|
|
1056
|
+
}
|
|
923
1057
|
function RemainingBar({ value, color, label }) {
|
|
924
1058
|
const pct = Math.max(0, Math.min(100, Number(value) || 0));
|
|
925
1059
|
return h("div", {
|
|
926
1060
|
role: "progressbar",
|
|
927
1061
|
"aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": label,
|
|
928
|
-
style: { width: "100%", height:
|
|
929
|
-
}, h("div", { style: { width: `${pct}%`, height: "100%", borderRadius:
|
|
1062
|
+
style: { width: "100%", height: 8, marginTop: 8, borderRadius: 99, background: TRACK, overflow: "hidden" }
|
|
1063
|
+
}, h("div", { style: { width: `${pct}%`, minWidth: pct > 0 ? 4 : 0, height: "100%", borderRadius: 99, background: color } }));
|
|
930
1064
|
}
|
|
931
1065
|
/**
|
|
932
1066
|
* 单窗口行(药丸弹层与设置页共用):徽标 + 余量% 主数字 + 进度条(满格=100% 剩余,随已用扣减)+ 明细行。
|
|
@@ -950,28 +1084,22 @@ window.__ModuleLoader__.load({
|
|
|
950
1084
|
resetLine(w, t, getLocale),
|
|
951
1085
|
w.cascade ? t("cascadeLimited") + (w.detail && w.detail.blockedBy ? ` [${w.detail.blockedBy.join(", ")}]` : "") : null
|
|
952
1086
|
].filter(Boolean).join(" · ");
|
|
1087
|
+
// 档位词只在「需要留意」时出现:充足时绿色长条与大数字已经说明一切,再贴一个「充足」只是噪音。
|
|
1088
|
+
const alert = tier.rank >= 2;
|
|
1089
|
+
const label = blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(left));
|
|
953
1090
|
return h("div", {
|
|
954
|
-
style: {
|
|
955
|
-
marginTop: 8, padding: "8px 10px", borderRadius: 8,
|
|
956
|
-
background: blocked ? "rgba(239,68,68,.08)" : "transparent",
|
|
957
|
-
border: blocked ? "1px solid rgba(239,68,68,.35)" : "1px solid transparent"
|
|
958
|
-
}
|
|
1091
|
+
style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
|
|
959
1092
|
},
|
|
960
1093
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
961
|
-
h("span", { style: { fontSize: 12
|
|
962
|
-
|
|
963
|
-
style: {
|
|
964
|
-
fontSize: 11, padding: "1px 8px", borderRadius: 999, whiteSpace: "nowrap",
|
|
965
|
-
border: `1px solid ${tier.color}`, color: tier.color
|
|
966
|
-
}
|
|
967
|
-
}, `${tier.icon} ${t(tier.badge)}`),
|
|
1094
|
+
h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, t(KIND_LABEL[w.kind] ?? "wall")),
|
|
1095
|
+
alert ? statusChip(t(tier.badge), tier.color) : null,
|
|
968
1096
|
h("span", { style: { flex: 1 } }),
|
|
969
|
-
h("strong", { style: { fontSize:
|
|
970
|
-
|
|
1097
|
+
h("strong", { style: { fontSize: 18, lineHeight: 1.1, color: alert ? textTone(tier.color) : "inherit", whiteSpace: "nowrap" } }, `${left}%`),
|
|
1098
|
+
h("span", { style: { fontSize: 11, opacity: 0.65, whiteSpace: "nowrap" } }, t("remainWord"))
|
|
971
1099
|
),
|
|
972
|
-
RemainingBar({ value: left, color: tier.color, label
|
|
1100
|
+
RemainingBar({ value: left, color: tier.color, label }),
|
|
973
1101
|
usageLine || resetParts
|
|
974
|
-
? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop:
|
|
1102
|
+
? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 6 } },
|
|
975
1103
|
usageLine ? h("div", { style: { overflowWrap: "anywhere" } }, usageLine) : null,
|
|
976
1104
|
resetParts ? h("div", { style: { marginTop: usageLine ? 2 : 0, overflowWrap: "anywhere" } }, resetParts) : null)
|
|
977
1105
|
: null
|
|
@@ -1070,72 +1198,53 @@ window.__ModuleLoader__.load({
|
|
|
1070
1198
|
}, [open]);
|
|
1071
1199
|
const rawEntry = snapshot && snapshot.entry;
|
|
1072
1200
|
if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const windows = entry ? entry.windows || [] : [];
|
|
1077
|
-
const worst = windows.length > 0 ? worstWindow(windows) : null;
|
|
1078
|
-
const worstTier = worst ? usageTier(worst) : null;
|
|
1079
|
-
const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
|
|
1201
|
+
// 整体状态由 computeStatus 一处判定:药丸、弹层横幅、设置页横幅读的是同一个结论。
|
|
1202
|
+
const st = computeStatus(rawEntry, { failed, failState, providerId, t, getLocale });
|
|
1203
|
+
const { entry, windows, worst, degraded, expiry, expiring } = st;
|
|
1080
1204
|
const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
: stateText === "no-cookie" ? t("pillNeedCookie")
|
|
1086
|
-
: null;
|
|
1087
|
-
const isConfig = stateText === "no-key" || stateText === "no-cookie";
|
|
1088
|
-
// 余额请求成功不代表有额度窗口(例如小米接口暂时只返回余额)。
|
|
1089
|
-
// Cookie 临近到期是需要重新登录的行动项:除额度已用尽外,优先于普通余量与降级提示。
|
|
1090
|
-
const expiry = cookieExpiry(rawEntry, t);
|
|
1091
|
-
const quotaBlocked = !!(worstTier && worstTier.rank >= 4);
|
|
1092
|
-
const expiring = !!(expiry && expiry.rank >= 2);
|
|
1093
|
-
// 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时
|
|
1094
|
-
// 主文案必须仍是失败原因,否则「还有 2 小时到期」会把重新登录推迟掉。
|
|
1095
|
-
// 例外是已过期:它本身就是「请重新登录」的行动项,比通用错误文案更具体。
|
|
1096
|
-
const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || (expiry && expiry.expired));
|
|
1097
|
-
// 底色与文字沿用额度告警语汇:已过期与额度用尽同为红,30 分钟内为橙,2 小时内为黄。
|
|
1098
|
-
const alertStyle = quotaBlocked || (expiry && expiry.expired) ? { background: "rgba(239,68,68,.10)", borderColor: "rgba(239,68,68,.45)" } : expiryActionable && expiry.rank >= 3 ? { background: "rgba(249,115,22,.10)", borderColor: "rgba(249,115,22,.45)" } : expiryActionable ? { background: "rgba(234,179,8,.10)", borderColor: "rgba(234,179,8,.45)" } : {};
|
|
1099
|
-
const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
|
|
1100
|
-
const triggerIcon = quotaBlocked ? worstTier.icon : expiryActionable ? expiry.icon : degraded ? "⚠" : worstTier ? worstTier.icon : ok ? "ℹ" : isConfig ? "⚙" : light;
|
|
1101
|
-
const triggerText = quotaBlocked ? (worst ? pillText(worst, windows.length > 1, t) : entry ? t("noUsageData") : stateLabel) : expiryActionable ? expiry.label : worst ? `${degraded ? statusText(rawEntry, t) + " · " : ""}${pillText(worst, windows.length > 1, t)}` : entry ? `${degraded ? stateLabel + " · " : ""}${(balanceText(entry, t) ?? t("noUsageData"))}` : isConfig ? t("needConfigShort") : stateLabel;
|
|
1205
|
+
// 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
|
|
1206
|
+
// 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
|
|
1207
|
+
const tip = `${label}: ${st.title}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
|
|
1208
|
+
const glyph = windows.length && st.key !== "cookie" && st.key !== "config" ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
|
|
1102
1209
|
return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
|
|
1103
1210
|
h("button", {
|
|
1104
1211
|
type: "button",
|
|
1105
1212
|
"aria-expanded": open,
|
|
1106
1213
|
"aria-haspopup": "dialog",
|
|
1107
|
-
"aria-label":
|
|
1108
|
-
title:
|
|
1214
|
+
"aria-label": tip,
|
|
1215
|
+
title: tip,
|
|
1109
1216
|
onClick: () => setOpen(!open),
|
|
1110
1217
|
style: {
|
|
1111
|
-
display: "inline-flex", alignItems: "center", gap:
|
|
1112
|
-
borderRadius: 999, border:
|
|
1113
|
-
|
|
1114
|
-
|
|
1218
|
+
display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
|
|
1219
|
+
borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
|
|
1220
|
+
background: st.tinted ? alpha(st.color, ".10") : "transparent",
|
|
1221
|
+
color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
|
|
1115
1222
|
}
|
|
1116
1223
|
},
|
|
1117
|
-
|
|
1118
|
-
h("span",
|
|
1119
|
-
failed && ok ? h("span", { style: { opacity: 0.7, fontSize: 11 } }, `(${t("unavailable")})`) : null
|
|
1224
|
+
glyph,
|
|
1225
|
+
st.short ? h("span", null, st.short) : null
|
|
1120
1226
|
),
|
|
1121
1227
|
open && h("div", {
|
|
1122
1228
|
role: "dialog",
|
|
1123
1229
|
"aria-label": `${label} ${t("nav")}`,
|
|
1124
1230
|
style: {
|
|
1125
1231
|
position: "absolute", bottom: "calc(100% + 6px)", right: 0, zIndex: 50,
|
|
1126
|
-
width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding:
|
|
1232
|
+
width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 14, borderRadius: 14,
|
|
1127
1233
|
background: "var(--dsh-surface, Canvas)", color: "var(--dsh-text, CanvasText)",
|
|
1128
|
-
border: "1px solid rgba(128,128,128,.
|
|
1129
|
-
boxShadow: "0
|
|
1234
|
+
border: "1px solid rgba(128,128,128,.28)",
|
|
1235
|
+
boxShadow: "0 12px 32px rgba(0,0,0,.28)", font: "12px/1.5 sans-serif"
|
|
1130
1236
|
}
|
|
1131
1237
|
},
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1238
|
+
// 标题行:厂商名 + 套餐名小标签。
|
|
1239
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1240
|
+
h("strong", { style: { fontSize: 13 } }, label),
|
|
1241
|
+
h("span", { style: { flex: 1 } }),
|
|
1242
|
+
planName(entry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(entry)) : null),
|
|
1243
|
+
// 状态横幅:先给结论(充足 / 偏少 / 即将用尽 / 已用尽 / 需配置 / 失败…)与一行依据。
|
|
1244
|
+
statusBanner(st),
|
|
1245
|
+
failed && st.key !== "error" && !st.needConfig ? h("p", { style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
|
|
1246
|
+
// Cookie 到期常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久(已在横幅里说明时不重复)。
|
|
1247
|
+
expiry && st.key !== "cookie" ? h("p", { role: "status", style: { margin: "8px 0 0", fontSize: 11, color: expiring ? textTone(expiry.color) : "inherit", opacity: expiring ? 1 : 0.75 } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
|
|
1139
1248
|
// 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
|
|
1140
1249
|
providerId === "commandcode" && ccState && ccState.status === "ready"
|
|
1141
1250
|
? h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.75 } },
|
|
@@ -1145,18 +1254,24 @@ window.__ModuleLoader__.load({
|
|
|
1145
1254
|
// 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
|
|
1146
1255
|
// 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
|
|
1147
1256
|
UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1257
|
+
// 余额等补充项:套餐名已在标题行,这里只剩「标签 … 数值」两列。
|
|
1258
|
+
entry && entry.extras && entry.extras.some((x) => x.kind !== "plan")
|
|
1259
|
+
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1260
|
+
entry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1261
|
+
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1262
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1151
1263
|
: null,
|
|
1152
1264
|
providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
|
|
1153
|
-
|
|
1154
|
-
h("
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1265
|
+
// 页脚:更新时间 + 刷新。
|
|
1266
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
1267
|
+
h("span", { style: { flex: 1, minWidth: 0, fontSize: 11, opacity: 0.6 } }, snapshot ? `${t("lastUpdated")} ${new Date(snapshot.updatedAt).toLocaleString(getLocale())}` : ""),
|
|
1268
|
+
h("button", {
|
|
1269
|
+
type: "button",
|
|
1270
|
+
onClick: () => retry.current(),
|
|
1271
|
+
disabled: refreshing,
|
|
1272
|
+
style: { padding: "4px 12px", borderRadius: 8, border: "none", background: alpha(INFO, ".14"), color: "inherit", font: "inherit", fontWeight: 600, cursor: refreshing ? "default" : "pointer", whiteSpace: "nowrap" }
|
|
1273
|
+
}, t(refreshing ? "refreshing" : "refresh"))
|
|
1274
|
+
)
|
|
1160
1275
|
)
|
|
1161
1276
|
);
|
|
1162
1277
|
}
|
|
@@ -1371,6 +1486,7 @@ window.__ModuleLoader__.load({
|
|
|
1371
1486
|
const usageMeta = PROVIDER_META[usageId];
|
|
1372
1487
|
// 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
|
|
1373
1488
|
const usageAction = usageActionKind(usageEntry, usageId, t);
|
|
1489
|
+
const usageStatus = computeStatus(usageEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
|
|
1374
1490
|
// 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
|
|
1375
1491
|
const revealEditor = (target) => {
|
|
1376
1492
|
openEditor(target);
|
|
@@ -1512,20 +1628,28 @@ window.__ModuleLoader__.load({
|
|
|
1512
1628
|
else if (event.key === "Home") next = 0; else if (event.key === "End") next = visibleIds.length - 1; else return;
|
|
1513
1629
|
event.preventDefault(); chooseUsage(visibleIds[next]);
|
|
1514
1630
|
const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
|
|
1515
|
-
}, style: { ...btnStyle,
|
|
1516
|
-
},
|
|
1631
|
+
}, style: { ...btnStyle, display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0, padding: "5px 12px 5px 8px", whiteSpace: "nowrap", fontSize: 12, fontWeight: active ? 600 : 400, borderRadius: 999, border: "1px solid transparent", background: active ? alpha(INFO, ".16") : "transparent" }
|
|
1632
|
+
}, statusGlyph(tagColor(e), providerIcon(e), 14), PROVIDER_META[id].short);
|
|
1517
1633
|
})),
|
|
1518
|
-
!!usageId ? h("section", { role: "tabpanel", id: "subusage-provider-panel", "aria-label": usageMeta.label, style: { marginTop: 12, border: "1px solid rgba(128,128,128,.25)", borderRadius:
|
|
1519
|
-
h("
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1634
|
+
!!usageId ? h("section", { role: "tabpanel", id: "subusage-provider-panel", "aria-label": usageMeta.label, style: { marginTop: 12, border: "1px solid rgba(128,128,128,.25)", borderRadius: 14, padding: 16 } },
|
|
1635
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1636
|
+
h("strong", { style: { fontSize: 14 } }, usageMeta.label),
|
|
1637
|
+
h("span", { style: { flex: 1 } }),
|
|
1638
|
+
planName(usageEntry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(usageEntry)) : null),
|
|
1639
|
+
// 一眼结论:与药丸、弹层同一个状态判定。读取成功只说明接口健康,额度档位另有结论。
|
|
1640
|
+
statusBanner(usageStatus),
|
|
1641
|
+
usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
|
|
1523
1642
|
usageAction ? h("div", { role: "group", "aria-label": text("建议操作", "Suggested action"), style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", margin: "10px 0", padding: "8px 10px", border: "1px solid rgba(234,179,8,.45)", borderRadius: 8, background: "rgba(234,179,8,.08)" } },
|
|
1524
1643
|
h("span", { style: { flex: 1, minWidth: 150, fontSize: 12 } }, usageAction === "mimo-relogin" ? text("Cookie 已过期,重新登录即可恢复。", "Cookie expired — sign in again to restore.") : usageAction === "mimo-login" ? text("需要登录 MiMo(或 Cookie 即将到期)。", "MiMo sign-in needed (or the cookie is about to expire).") : text("这家还没有可用的凭据。", "No usable credentials for this provider.")),
|
|
1525
1644
|
usageAction !== "credentials" ? h("button", { type: "button", style: primary, disabled: busy || loginActive, onClick: startLogin }, usageAction === "mimo-relogin" ? text("重新登录", "Sign in again") : text("登录并自动导入", "Sign in & import")) : null,
|
|
1526
1645
|
h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
|
|
1527
1646
|
canShowUsage(usageEntry) && (usageEntry.windows || []).length ? UsageWindowList({ windows: usageEntry.windows, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" }) : h("p", { style: { opacity: .75 } }, usageEntry ? t("noUsageData") : t("loading")),
|
|
1528
|
-
canShowUsage(usageEntry) && usageEntry.extras
|
|
1647
|
+
canShowUsage(usageEntry) && usageEntry.extras && usageEntry.extras.some((x) => x.kind !== "plan")
|
|
1648
|
+
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1649
|
+
usageEntry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1650
|
+
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1651
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1652
|
+
: null,
|
|
1529
1653
|
usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null
|
|
1530
1654
|
) : null,
|
|
1531
1655
|
providerManagement
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.11",
|
|
4
4
|
"displayName": "订阅用量显示",
|
|
5
5
|
"description": "显示 DeepSeek 官方余额、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
6
6
|
"keywords": [
|