dsh-subusage 0.10.9 → 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 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
- [![npm version](https://img.shields.io/badge/npm-0.10.9-blue)](https://www.npmjs.com/package/dsh-subusage)
9
+ [![npm version](https://img.shields.io/badge/npm-0.10.11-blue)](https://www.npmjs.com/package/dsh-subusage)
10
10
  [![license](https://img.shields.io/badge/license-MIT-green)](#license--security--许可与安全)
11
11
  [![DSH](https://img.shields.io/badge/DSH-%3E%3D0.2.0--rc.1%20%3C0.3.0--0-informational)](#compatibility--兼容性)
12
12
 
@@ -18,7 +18,7 @@
18
18
  | 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
19
19
  | 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
20
20
 
21
- 药丸示例:`✓ 余 87%`、`⚠ 7d 余 12%`、`✕ 7d 已达限额`。多窗口取最差一窗,点击查看完整明细;成功结果按提供商缓存 **60 秒**,手动刷新可跳过普通 TTL(仍遵守上游限流退避与 `Retry-After`)。
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.7.0 的离线组件测试截图,使用虚构用量数据,不包含真实账号信息,也不是运行中的 DSH 截图。
388
+ 以下为 0.10.11 的离线组件测试截图,使用虚构用量数据,不包含真实账号信息,也不是运行中的 DSH 截图。
389
389
 
390
390
  深色设置页:查看订阅用量、管理凭据与提供商。
391
391
 
392
392
  ![深色订阅设置页(离线测试预览)](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/settings-dark.png)
393
393
 
394
- 浅色用量弹层:查看每日与每周额度、用量明细。
394
+ 浅色用量弹层:顶部先给结论(额度充足 / 余量偏低 / 即将用尽…),下面是各窗口的剩余百分比与重置时间。右下角是收起态药丸——每个窗口一根彩条,正常时不写字。
395
395
 
396
396
  ![浅色 NanoGPT 用量弹层(离线测试预览)](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/usage-popover-light.png)
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
- badgeOk: "可用",
95
- badgeHigh: "偏高",
96
- badgeTight: "紧张",
97
- badgeLimited: "不可用",
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; quota unknown",
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
- badgeOk: "OK",
208
- badgeHigh: "High",
209
- badgeTight: "Tight",
210
- badgeLimited: "Unavailable",
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: "Limit reached",
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.",
@@ -389,13 +410,16 @@ window.__ModuleLoader__.load({
389
410
  }
390
411
  return worst;
391
412
  }
413
+ function remainingPercent(percent) {
414
+ return Math.round(Math.max(0, 100 - (Number(percent) || 0)) * 10) / 10;
415
+ }
392
416
  /** 药丸文案:永远答「还能用多少」(余),限额/连坐直说。多窗带窗口短名。 */
393
417
  function pillText(w, multi, t) {
394
418
  // 短标签走 locale(中文「周 / 月 / 日」,英文 1w / 1m / 1d),不让人猜 W 是哪个窗口。
395
419
  const short = multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
396
420
  if (w.status === "rate-limited" && !w.cascade) return `${short}${t("pillLimited")}`;
397
421
  if (w.status === "rate-limited") return `${short}${t("pillBlocked")}`;
398
- return `${short}${t("pillRemaining").replace("{p}", String(Math.round(Math.max(0, 100 - w.percent) * 10) / 10))}`;
422
+ return `${short}${t("pillRemaining").replace("{p}", String(remainingPercent(w.percent)))}`;
399
423
  }
400
424
  /** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
401
425
  function extraLabel(x, t) {
@@ -917,14 +941,136 @@ window.__ModuleLoader__.load({
917
941
  );
918
942
  }
919
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
+ }
1057
+ function RemainingBar({ value, color, label }) {
1058
+ const pct = Math.max(0, Math.min(100, Number(value) || 0));
1059
+ return h("div", {
1060
+ role: "progressbar",
1061
+ "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": label,
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 } }));
1064
+ }
920
1065
  /**
921
- * 单窗口行(药丸弹层与设置页共用):徽标 + 已用% 主数字 + 进度条 + 明细行。
1066
+ * 单窗口行(药丸弹层与设置页共用):徽标 + 余量% 主数字 + 进度条(满格=100% 剩余,随已用扣减)+ 明细行。
922
1067
  * 明细分两行:用量/适用范围一行,重置时间一行。挤在一行时「重置于 …(还有 …)」
923
1068
  * 会在窄弹层里被从中间折断,读起来像属性和数值分了家。
924
1069
  */
925
1070
  function UsageWindowRow({ w, t, getLocale, uncertain = false }) {
926
1071
  const tier = uncertain && w.status !== "rate-limited" ? { icon: "?", badge: "badgeUnknown", color: "#b7791f", rank: 2 } : usageTier(w);
927
1072
  const blocked = w.status === "rate-limited";
1073
+ const left = blocked ? 0 : remainingPercent(w.percent);
928
1074
  const usageLine = [
929
1075
  w.detail && typeof w.detail.used === "number" && typeof w.detail.limit === "number" && w.detail.limit > 0
930
1076
  ? t("detailUsed").replace("{used}", fmtTokens(w.detail.used)).replace("{limit}", fmtTokens(w.detail.limit)).replace("{unit}", w.detail.unit || "")
@@ -938,31 +1084,22 @@ window.__ModuleLoader__.load({
938
1084
  resetLine(w, t, getLocale),
939
1085
  w.cascade ? t("cascadeLimited") + (w.detail && w.detail.blockedBy ? ` [${w.detail.blockedBy.join(", ")}]` : "") : null
940
1086
  ].filter(Boolean).join(" · ");
1087
+ // 档位词只在「需要留意」时出现:充足时绿色长条与大数字已经说明一切,再贴一个「充足」只是噪音。
1088
+ const alert = tier.rank >= 2;
1089
+ const label = blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(left));
941
1090
  return h("div", {
942
- style: {
943
- marginTop: 8, padding: "8px 10px", borderRadius: 8,
944
- background: blocked ? "rgba(239,68,68,.08)" : "transparent",
945
- border: blocked ? "1px solid rgba(239,68,68,.35)" : "1px solid transparent"
946
- }
1091
+ style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
947
1092
  },
948
1093
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
949
- h("span", { style: { fontSize: 12, opacity: 0.85, minWidth: 52 } }, t(KIND_LABEL[w.kind] ?? "wall")),
950
- h("span", {
951
- style: {
952
- fontSize: 11, padding: "1px 8px", borderRadius: 999, whiteSpace: "nowrap",
953
- border: `1px solid ${tier.color}`, color: tier.color
954
- }
955
- }, `${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,
956
1096
  h("span", { style: { flex: 1 } }),
957
- h("strong", { style: { fontSize: 13, color: numColor(w), whiteSpace: "nowrap" } },
958
- blocked ? t(tier.num) : t("numUsed").replace("{p}", String(w.percent)))
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"))
959
1099
  ),
960
- h("progress", {
961
- max: 100, value: Math.min(100, w.percent),
962
- style: { width: "100%", height: 6, marginTop: 6, accentColor: tier.color }
963
- }),
1100
+ RemainingBar({ value: left, color: tier.color, label }),
964
1101
  usageLine || resetParts
965
- ? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 4 } },
1102
+ ? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 6 } },
966
1103
  usageLine ? h("div", { style: { overflowWrap: "anywhere" } }, usageLine) : null,
967
1104
  resetParts ? h("div", { style: { marginTop: usageLine ? 2 : 0, overflowWrap: "anywhere" } }, resetParts) : null)
968
1105
  : null
@@ -1061,72 +1198,53 @@ window.__ModuleLoader__.load({
1061
1198
  }, [open]);
1062
1199
  const rawEntry = snapshot && snapshot.entry;
1063
1200
  if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
1064
- const entry = canShowUsage(rawEntry) ? rawEntry : null;
1065
- const ok = !!(rawEntry && rawEntry.state === "ok" && rawEntry.freshness !== "stale" && rawEntry.coverage !== "partial");
1066
- const light = ok ? "🟢" : (snapshot !== null || failed !== null ? "🔴" : "🟡");
1067
- const windows = entry ? entry.windows || [] : [];
1068
- const worst = windows.length > 0 ? worstWindow(windows) : null;
1069
- const worstTier = worst ? usageTier(worst) : null;
1070
- 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;
1071
1204
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1072
- const stateText = !ok ? (snapshot && snapshot.entry ? snapshot.entry.state : failState) : void 0;
1073
- const stateLabel = rawEntry ? statusText(rawEntry, t) : failState === "no-key" ? t("statusNoKey") : failState === "no-cookie" ? t("statusNoCookie") : failed ? t("statusError") : t("loading");
1074
- // 配置缺失类失败给可行动指引,不倒裸报错。
1075
- const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
1076
- : stateText === "no-cookie" ? t("pillNeedCookie")
1077
- : null;
1078
- const isConfig = stateText === "no-key" || stateText === "no-cookie";
1079
- // 余额请求成功不代表有额度窗口(例如小米接口暂时只返回余额)。
1080
- // Cookie 临近到期是需要重新登录的行动项:除额度已用尽外,优先于普通余量与降级提示。
1081
- const expiry = cookieExpiry(rawEntry, t);
1082
- const quotaBlocked = !!(worstTier && worstTier.rank >= 4);
1083
- const expiring = !!(expiry && expiry.rank >= 2);
1084
- // 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时
1085
- // 主文案必须仍是失败原因,否则「还有 2 小时到期」会把重新登录推迟掉。
1086
- // 例外是已过期:它本身就是「请重新登录」的行动项,比通用错误文案更具体。
1087
- const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || (expiry && expiry.expired));
1088
- // 底色与文字沿用额度告警语汇:已过期与额度用尽同为红,30 分钟内为橙,2 小时内为黄。
1089
- 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)" } : {};
1090
- const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
1091
- const triggerIcon = quotaBlocked ? worstTier.icon : expiryActionable ? expiry.icon : degraded ? "⚠" : worstTier ? worstTier.icon : ok ? "ℹ" : isConfig ? "⚙" : light;
1092
- 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);
1093
1209
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
1094
1210
  h("button", {
1095
1211
  type: "button",
1096
1212
  "aria-expanded": open,
1097
1213
  "aria-haspopup": "dialog",
1098
- "aria-label": `${label}: ${stateLabel} · ${summary}${expiring && !quotaBlocked ? " · " + expiry.label : ""}`,
1099
- title: `${label}: ${stateLabel} · ${summary}${expiring && !quotaBlocked ? " · " + expiry.label : ""}`,
1214
+ "aria-label": tip,
1215
+ title: tip,
1100
1216
  onClick: () => setOpen(!open),
1101
1217
  style: {
1102
- display: "inline-flex", alignItems: "center", gap: 6, padding: "4px 10px",
1103
- borderRadius: 999, border: "1px solid rgba(128,128,128,.35)", background: "transparent",
1104
- color: "inherit", font: "inherit", cursor: "pointer", whiteSpace: "nowrap",
1105
- ...alertStyle
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"
1106
1222
  }
1107
1223
  },
1108
- h("span", { "aria-hidden": "true", style: { fontSize: 11, lineHeight: 1 } }, triggerIcon),
1109
- h("span", { style: triggerColor ? { color: triggerColor } : null }, triggerText),
1110
- failed && ok ? h("span", { style: { opacity: 0.7, fontSize: 11 } }, `(${t("unavailable")})`) : null
1224
+ glyph,
1225
+ st.short ? h("span", null, st.short) : null
1111
1226
  ),
1112
1227
  open && h("div", {
1113
1228
  role: "dialog",
1114
1229
  "aria-label": `${label} ${t("nav")}`,
1115
1230
  style: {
1116
1231
  position: "absolute", bottom: "calc(100% + 6px)", right: 0, zIndex: 50,
1117
- width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 12, borderRadius: 10,
1232
+ width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 14, borderRadius: 14,
1118
1233
  background: "var(--dsh-surface, Canvas)", color: "var(--dsh-text, CanvasText)",
1119
- border: "1px solid rgba(128,128,128,.35)",
1120
- boxShadow: "0 8px 24px rgba(0,0,0,.35)", font: "12px/1.6 sans-serif"
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"
1121
1236
  }
1122
1237
  },
1123
- h("strong", null, label),
1124
- needConfig ? h("p", { style: { margin: "6px 0 0", opacity: 0.9 } }, needConfig) : null,
1125
- failed && !needConfig ? h("p", { style: { color: "#f87171", margin: "6px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
1126
- !ok && !snapshot && !failed ? h("p", { style: { margin: "6px 0 0", opacity: 0.8 } }, t("loading")) : null,
1127
- cacheAge(rawEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(rawEntry, t)) : null,
1128
- // 到期提醒常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久。
1129
- expiry ? h("p", { role: "status", style: { margin: "6px 0 0", color: expiring ? expiry.color : "inherit" } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
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,
1130
1248
  // 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
1131
1249
  providerId === "commandcode" && ccState && ccState.status === "ready"
1132
1250
  ? h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.75 } },
@@ -1136,18 +1254,24 @@ window.__ModuleLoader__.load({
1136
1254
  // 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
1137
1255
  // 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
1138
1256
  UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
1139
- entry && entry.extras && entry.extras.length > 0
1140
- ? h("div", { style: { marginTop: 10, opacity: 0.85 } },
1141
- entry.extras.map((x) => h("div", { key: x.kind }, `${extraLabel(x, t)}: ${x.value}`)))
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))))
1142
1263
  : null,
1143
1264
  providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1144
- snapshot ? h("div", { style: { marginTop: 10, opacity: 0.7 } }, `${t("lastUpdated")} ${new Date(snapshot.updatedAt).toLocaleString(getLocale())}`) : null,
1145
- h("button", {
1146
- type: "button",
1147
- onClick: () => retry.current(),
1148
- disabled: refreshing,
1149
- style: { marginTop: 10, padding: "4px 12px", borderRadius: 6, border: "1px solid rgba(128,128,128,.5)", background: "transparent", color: "inherit", cursor: "pointer" }
1150
- }, t(refreshing ? "refreshing" : "retry"))
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
+ )
1151
1275
  )
1152
1276
  );
1153
1277
  }
@@ -1362,6 +1486,7 @@ window.__ModuleLoader__.load({
1362
1486
  const usageMeta = PROVIDER_META[usageId];
1363
1487
  // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1364
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 });
1365
1490
  // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1366
1491
  const revealEditor = (target) => {
1367
1492
  openEditor(target);
@@ -1503,20 +1628,28 @@ window.__ModuleLoader__.load({
1503
1628
  else if (event.key === "Home") next = 0; else if (event.key === "End") next = visibleIds.length - 1; else return;
1504
1629
  event.preventDefault(); chooseUsage(visibleIds[next]);
1505
1630
  const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
1506
- }, style: { ...btnStyle, minWidth: 0, padding: "7px 10px", whiteSpace: "nowrap", fontSize: 12, borderRadius: "6px 6px 0 0", border: "1px solid transparent", borderBottom: active ? "2px solid #3b82f6" : "2px solid transparent", background: active ? "rgba(59,130,246,.10)" : "transparent" }
1507
- }, h("span", { "aria-hidden": true, style: { display: "inline-block", width: 6, height: 6, marginRight: 4, borderRadius: 99, background: tagColor(e) } }), h("span", { "aria-hidden": true, style: { marginRight: 3 } }, providerIcon(e)), PROVIDER_META[id].short);
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);
1508
1633
  })),
1509
- !!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: 10, padding: 14 } },
1510
- h("strong", null, usageMeta.label),
1511
- h("p", { role: "status", style: { margin: "4px 0", color: usageEntry && usageEntry.state !== "ok" ? "#dc2626" : "inherit" } }, statusText(usageEntry, t)),
1512
- cacheAge(usageEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(usageEntry, t)) : null,
1513
- usageEntry && usageEntry.error ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
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,
1514
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)" } },
1515
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.")),
1516
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,
1517
1645
  h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1518
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")),
1519
- canShowUsage(usageEntry) && usageEntry.extras ? h("p", { style: { opacity: .75 } }, usageEntry.extras.map((x) => `${extraLabel(x, t)}: ${x.value}`).join(" · ")) : null,
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,
1520
1653
  usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null
1521
1654
  ) : null,
1522
1655
  providerManagement
package/lib/index.js CHANGED
@@ -411,7 +411,7 @@ function volcFailure(action, error) {
411
411
  }
412
412
  async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
413
413
  let response;
414
- try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.9", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
414
+ try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.10", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
415
415
  catch { throw failure("subusage/network", "网络请求失败或超时", true); }
416
416
  if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
417
417
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subusage",
3
- "version": "0.10.9",
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": [