dsh-subusage 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -62,6 +62,8 @@ window.__ModuleLoader__.load({
62
62
  unavailable: "暂不可用",
63
63
  planLabel: "套餐",
64
64
  balanceLabel: "余额",
65
+ balanceLow: "余额偏低",
66
+ pillNeedDeepseekAccount: "请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key。",
65
67
  spendLabel: "已用",
66
68
  monthlyBalanceLabel: "月额度剩余",
67
69
  planSnapshotLimit: "月总额来自套餐快照,不含已购/赠送余额;未知套餐不推算上限",
@@ -122,6 +124,17 @@ window.__ModuleLoader__.load({
122
124
  pillCookieSoon: "Cookie {rest}后到期",
123
125
  mimoCookieExpiresAt: "{time} 到期",
124
126
  noUsageData: "暂无额度数据",
127
+ statusPaygo: "可按量续用",
128
+ statusPaygoSub: "已购余额或计费接口支持按量续用;实际请求与余额仍以服务端为准",
129
+ statusIncludedAdvice: "基础额度已用尽,接口仍允许订阅续用",
130
+ statusAvailabilityUnknown: "续用条件待确认",
131
+ statusRestricted: "账户或预算已受限",
132
+ statusScopeLimited: "部分能力额度已用尽",
133
+ statusPoolAvailable: "轮换池仍有可用账户", statusPoolAvailableShort: "池可用", scopeAllShort: "共享额度",
134
+ statusPoolUnknown: "轮换池可用性待确认",
135
+ budgetKey: "Key 预算", budgetUser: "用户预算", budgetTeam: "团队预算", budgetScope: "上级预算",
136
+ noteLabel: "说明", creditStatusLabel: "Credits 状态", statusUnlimited: "已报告的套餐窗口不设限",
137
+ statusPaygoShort: "按量续用", statusIncludedShort: "仍可续用", statusUnknownShort: "续用待确认", statusScopeShort: "部分能力用尽", statusRestrictedShort: "已受限",
125
138
  providerUnavailable: "用量暂不可用",
126
139
  pillNeedCommandcode: "需配置 —— 可在 设置 → Command Code 登录、运行 cmd login,或导出 COMMANDCODE_API_KEY 后重试。",
127
140
  pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
@@ -142,8 +155,9 @@ window.__ModuleLoader__.load({
142
155
  ccSwitchFailed: "切换失败",
143
156
  // Command Code 自动轮换池健康度(其余账户用量由 Host 并行读取,各自缓存 60 秒)。
144
157
  ccPoolTitle: "轮换账户池",
158
+ ccPoolDetails: "点击查看详情", ccPoolWindowLimited: "{windows}额度已用尽", ccPoolLimitUnknown: "额度受限(周期未知)", ccPoolPeriodUnknown: "未知周期",
145
159
  ccPoolAllHealthy: "{n} 个账户均可轮换使用",
146
- ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个已达限额",
160
+ ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个账户有额度窗口受限",
147
161
  ccPoolUnusable: "{unusable} 个账户无可用凭据或暂时读不到",
148
162
  ccPoolExhausted: "轮换账户均已达限额,新请求很可能被拒绝",
149
163
  ccPoolExhaustedShort: "池已满限额",
@@ -207,6 +221,8 @@ window.__ModuleLoader__.load({
207
221
  unavailable: "Unavailable",
208
222
  planLabel: "Plan",
209
223
  balanceLabel: "Balance",
224
+ balanceLow: "Low balance",
225
+ pillNeedDeepseekAccount: "Sign in under DSH Settings → Account & balance; this entry does not use an API key.",
210
226
  spendLabel: "Spent",
211
227
  monthlyBalanceLabel: "Monthly credits remaining",
212
228
  planSnapshotLimit: "Monthly total uses a plan snapshot, excluding purchased/free credits; unknown plans have no estimated cap",
@@ -266,6 +282,17 @@ window.__ModuleLoader__.load({
266
282
  pillCookieSoon: "Cookie expires in {rest}",
267
283
  mimoCookieExpiresAt: "Expires {time}",
268
284
  noUsageData: "No quota data",
285
+ statusPaygo: "Paid usage available",
286
+ statusPaygoSub: "Purchased balance or billing advice permits paid usage; the server and actual balance remain authoritative",
287
+ statusIncludedAdvice: "Base quota used up; subscription usage still advised",
288
+ statusAvailabilityUnknown: "Continuation conditions not confirmed",
289
+ statusRestricted: "Account or budget restricted",
290
+ statusScopeLimited: "Some capability quotas used up",
291
+ statusPoolAvailable: "Rotation pool still has usable accounts", statusPoolAvailableShort: "pool usable", scopeAllShort: "shared quota",
292
+ statusPoolUnknown: "Rotation pool availability not confirmed",
293
+ budgetKey: "Key budget", budgetUser: "User budget", budgetTeam: "Team budget", budgetScope: "Parent budget",
294
+ noteLabel: "Note", creditStatusLabel: "Credits status", statusUnlimited: "Reported plan windows are unlimited",
295
+ statusPaygoShort: "paid usage", statusIncludedShort: "usage advised", statusUnknownShort: "check continuation", statusScopeShort: "some quotas used up", statusRestrictedShort: "restricted",
269
296
  providerUnavailable: "Usage unavailable",
270
297
  pillNeedCommandcode: "Setup required — sign in under Settings → Command Code, run cmd login, or export COMMANDCODE_API_KEY and retry.",
271
298
  pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
@@ -286,8 +313,9 @@ window.__ModuleLoader__.load({
286
313
  ccSwitchFailed: "Switch failed",
287
314
  // Command Code rotation-pool health (the Host reads the other accounts in parallel, each cached 60s).
288
315
  ccPoolTitle: "Rotation account pool",
316
+ ccPoolDetails: "Click to view details", ccPoolWindowLimited: "{windows} quota used up", ccPoolLimitUnknown: "Quota restricted (period unknown)", ccPoolPeriodUnknown: "Unknown period",
289
317
  ccPoolAllHealthy: "All {n} accounts are available",
290
- ccPoolCapacity: "{available}/{total} accounts available, {blocked} at their limit",
318
+ ccPoolCapacity: "{available}/{total} accounts available, {blocked} with limited quota windows",
291
319
  ccPoolUnusable: "{unusable} account(s) have no usable credentials or could not be read",
292
320
  ccPoolExhausted: "Every rotation account is at its limit; new requests are likely to be rejected",
293
321
  ccPoolExhaustedShort: "pool at limit",
@@ -313,7 +341,8 @@ window.__ModuleLoader__.load({
313
341
  const KIND_SHORT = { "5h": "s5h", "7d": "s7d", day: "sday", week: "sweek", month: "smonth", period: "speriod", sub: "ssub", rolling: "srolling" };
314
342
  const PROVIDER_META = {
315
343
  // 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
316
- "deepseek": { label: "DeepSeek", short: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
344
+ "deepseek": { label: "DeepSeek API", short: "DeepSeek API", envName: "DEEPSEEK_API_KEY" },
345
+ "deepseek-account": { label: "DeepSeek 账号", short: "DeepSeek 账号", envName: "", managedByPlugin: true },
317
346
  "zai-coding": { label: "Z.ai Coding (International)", short: "Z.ai Intl", envName: "ZAI_CODING_API_KEY", defaultEnabled: false },
318
347
  "synthetic": { label: "Synthetic", short: "Synthetic", envName: "SYNTHETIC_API_KEY", defaultEnabled: false },
319
348
  "nanogpt": { label: "NanoGPT", short: "NanoGPT", envName: "NANOGPT_API_KEY", defaultEnabled: false },
@@ -361,7 +390,7 @@ window.__ModuleLoader__.load({
361
390
  "litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
362
391
  };
363
392
  // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
364
- const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
393
+ const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek-account", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
365
394
  // 火山方舟的 4 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
366
395
  // 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
367
396
  const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
@@ -372,7 +401,7 @@ window.__ModuleLoader__.load({
372
401
  * 但装了旧插件的用户,模型路由仍然是这两个 id —— 药丸必须在入口把它们归并到 arkcli 路由,
373
402
  * 否则那些用户会突然失去用量显示。(第三条 BytePlus 已随实现一并移除:无 BytePlus 账号可验证。)
374
403
  */
375
- const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan" };
404
+ const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan", "deepseek-official": "deepseek-account" };
376
405
  function providerEnabled(settings, id) {
377
406
  if (PROVIDER_META[id]?.volc) {
378
407
  // 组开关优先;旧配置里没有它时按「组内任一条开着」合并过来,全新配置则沿用默认。
@@ -386,6 +415,10 @@ window.__ModuleLoader__.load({
386
415
  // 默认收起比「常量里默认开启」更贴近事实。undefined = 拿不到路由表,按「未知」放行,不谎报未安装。
387
416
  // 例外是火山方舟:它的额度走账号级管控面(一组 AK/SK 覆盖名下所有套餐),路由只是推理入口 —— 见 providerVisible。
388
417
  function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
418
+ const BALANCE_ONLY_IDS = new Set(["deepseek", "deepseek-account", "siliconflow", "novita", "hyperbolic", "deepinfra", "vercel-ai-gateway"]);
419
+ function providerCanQuery(result, id) {
420
+ return BALANCE_ONLY_IDS.has(id) || !!PROVIDER_META[id]?.volc || !!result?.settings?.hasKeys?.[id] || result?.settings?.keyModes?.[id] === "manual" || providerRoutable(result, id);
421
+ }
389
422
  function providerVisible(settings, id, entry, result) {
390
423
  if (!providerEnabled(settings, id)) return false;
391
424
  // 关掉「没有检测到 API 的默认隐藏」就是要看全部,不再按路由、凭据或订阅状态收起。
@@ -396,7 +429,9 @@ window.__ModuleLoader__.load({
396
429
  // `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
397
430
  if (PROVIDER_META[id]?.volc) {
398
431
  if (entry?.subscribed === false) return false;
399
- } else if (!providerRoutable(result, id)) return false;
432
+ } else if (!providerCanQuery(result, id)) return false;
433
+ // 官方余额与本机账号余额独立于模型路由;刚保存 Key 的旧 no-key 条目不能继续隐藏。
434
+ if (settings?.hasKeys?.[id]) return true;
400
435
  return entry?.apiDetected !== false && !["no-key", "no-cookie"].includes(entry?.state);
401
436
  }
402
437
  /** token 数紧凑格式:1,234,567,890 → 1.23B */
@@ -446,12 +481,30 @@ window.__ModuleLoader__.load({
446
481
  }
447
482
  /** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
448
483
  function extraLabel(x, t) {
449
- return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : x.kind === "spend" ? t("spendLabel") : t("balanceLabel");
484
+ return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : ["note", "quota-status"].includes(x.kind) ? t("noteLabel") : x.kind === "credit-status" ? t("creditStatusLabel") : x.kind === "spend" || x.kind.endsWith("-spend") ? t("spendLabel") : t("balanceLabel");
450
485
  }
451
486
  /** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
452
487
  function balanceText(entry, t) {
453
- const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") : undefined;
454
- return row ? `${t("balanceLabel")} ${row.value}` : null;
488
+ const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
489
+ return row ? `${extraLabel(row, t)} ${row.value}` : null;
490
+ }
491
+ /** 余额只按自己的单位预警,不做汇率折算,也不把累计消费当余额。 */
492
+ function balanceVisual(entry, t) {
493
+ const row = Array.isArray(entry?.extras) ? entry.extras.find(x => x.kind === "balance" && typeof x.value === "string") : undefined;
494
+ if (!row) return null;
495
+ const text = row.value.trim();
496
+ const decimal = "([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?)";
497
+ const suffix = text.match(new RegExp("^" + decimal + "\\s+([A-Z]{3}|credits)$", "i"));
498
+ const prefix = text.match(new RegExp("^([¥¥$])\\s*" + decimal + "$", "i"));
499
+ const bare = text.match(new RegExp("^" + decimal + "$", "i"));
500
+ const amount = suffix ? Number(suffix[1]) : prefix ? Number(prefix[2]) : bare ? Number(bare[1]) : NaN;
501
+ const unit = suffix ? suffix[2].toUpperCase() : prefix ? prefix[1] === "$" ? "USD" : "CNY" : null;
502
+ const rawAmount = suffix?.[1] ?? prefix?.[2];
503
+ const value = unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text;
504
+ // 产品预警阈值,不是汇率或能运行多少次的保证。credits 没有跨厂商统一价值。
505
+ const limits = unit === "CNY" ? { red: 1, amber: 5 } : unit === "USD" ? { red: 0.2, amber: 1 } : null;
506
+ const rank = Number.isFinite(amount) ? amount <= 0 || limits && amount < limits.red ? 2 : limits && amount < limits.amber ? 1 : 0 : 0;
507
+ return { value, color: rank === 2 ? "#ef4444" : rank === 1 ? "#eab308" : "#94a3b8", tinted: rank > 0, label: rank > 0 ? t("balanceLow") : "" };
455
508
  }
456
509
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
457
510
  function durationText(ms, t) {
@@ -710,13 +763,11 @@ window.__ModuleLoader__.load({
710
763
  */
711
764
  function usageAttentionRank(entry, t) {
712
765
  if (!entry) return 0;
713
- if (entry.state === "error") return 3;
714
- const expiry = cookieExpiry(entry, t);
715
- if (expiry && expiry.expired) return 3;
716
- if ((entry.windows || []).some(w => w.status === "rate-limited")) return 2;
717
- if (expiry && expiry.rank >= 2) return 2;
718
- if (entry.freshness === "stale" || entry.coverage === "partial") return 1;
719
- return 0;
766
+ const status = computeStatus(entry, { providerId: entry.providerId, t, getLocale: () => undefined });
767
+ if (status.key === "error" || status.key === "cookie" && status.expiry?.expired) return 3;
768
+ if (["blocked", "restricted", "cookie"].includes(status.key)) return 2;
769
+ if (["paygo", "included", "unknown", "scope", "pool-unknown", "degraded"].includes(status.key)) return 1;
770
+ return status.key === "pool" && (entry.accountPool.blockedCount || entry.accountPool.unusableCount) ? 1 : 0;
720
771
  }
721
772
  function providerDescription(entry, t) {
722
773
  const windows = canShowUsage(entry) ? entry.windows || [] : [];
@@ -767,7 +818,7 @@ window.__ModuleLoader__.load({
767
818
  // 全部静默不发请求,用户点「刷新当前」也空转,只有「刷新全部」走 read 才能更新。
768
819
  const unique = [...new Set(ids)].filter((id) => {
769
820
  if (!PROVIDER_ORDER.includes(id) || !providerEnabled(snapshot.settings, id)) return false;
770
- return PROVIDER_META[id]?.volc ? true : providerRoutable(snapshot, id);
821
+ return providerCanQuery(snapshot, id);
771
822
  });
772
823
  const start = (id) => {
773
824
  if (inflight.has(id)) return inflight.get(id);
@@ -1030,21 +1081,25 @@ window.__ModuleLoader__.load({
1030
1081
  if (!pool || pool.mode !== "auto" || !Array.isArray(pool.accounts) || pool.total <= 1) return null;
1031
1082
  const summary = pool.exhausted
1032
1083
  ? t("ccPoolExhausted")
1033
- : pool.blockedCount > 0
1084
+ : pool.blockedCount > 0 || pool.unusableCount > 0
1034
1085
  ? t("ccPoolCapacity").replace("{available}", pool.available).replace("{total}", pool.total).replace("{blocked}", pool.blockedCount)
1035
1086
  : t("ccPoolAllHealthy").replace("{n}", pool.total);
1036
- const tone = pool.exhausted ? "#ef4444" : pool.blockedCount > 0 ? "#eab308" : "#22c55e";
1087
+ const tone = pool.exhausted ? "#ef4444" : pool.blockedCount > 0 || pool.unusableCount > 0 ? "#eab308" : "#22c55e";
1037
1088
  const rowFor = (account) => {
1038
1089
  const worst = worstWindow(account.windows || []);
1039
- const tier = account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
1090
+ const tier = account.availability === "paygo" ? { color: "#eab308", icon: "ℹ" } : account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
1040
1091
  const blocked = account.blocked;
1092
+ const limited = (account.windows || []).filter(w => w.status === "rate-limited");
1093
+ const roots = limited.filter(w => !w.cascade);
1094
+ const kinds = [...new Set(roots.length ? roots.map(w => w.kind) : limited.flatMap(w => w.detail?.blockedBy?.length ? w.detail.blockedBy : [w.kind]))];
1095
+ const limitDetail = kinds.length ? t("ccPoolWindowLimited").replace("{windows}", kinds.map(kind => t(KIND_LABEL[kind] || "ccPoolPeriodUnknown")).join(" / ")) : t("ccPoolLimitUnknown");
1041
1096
  const unusable = !account.usable && !account.blocked;
1042
1097
  const dot = statusGlyph(unusable ? NEUTRAL : (tier ? tier.color : NEUTRAL), unusable ? "?" : blocked ? "✕" : (tier ? tier.icon : "?"), 14);
1043
1098
  const detail = unusable
1044
- ? (account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
1099
+ ? (account.availability === "unknown" ? t("statusUnknownShort") : account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
1045
1100
  : blocked
1046
- ? t("pillLimited")
1047
- : worst ? pillText(worst, (account.windows || []).length > 1, t)
1101
+ ? limitDetail
1102
+ : account.availability === "paygo" ? t("statusPaygoShort") : worst ? pillText(worst, (account.windows || []).length > 1, t)
1048
1103
  : t("noUsageData");
1049
1104
  const canPin = cc && account.id !== "default";
1050
1105
  return h("button", {
@@ -1055,7 +1110,7 @@ window.__ModuleLoader__.load({
1055
1110
  },
1056
1111
  dot,
1057
1112
  h("span", { style: { flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, account.id === "default" ? t("ccAccountDefault") : account.label),
1058
- h("span", { style: { opacity: 0.75, whiteSpace: "nowrap" } }, detail));
1113
+ h("span", { style: { opacity: 0.75, flex: "0 1 60%", minWidth: 0, textAlign: "right", whiteSpace: "normal", overflowWrap: "anywhere" } }, detail));
1059
1114
  };
1060
1115
  return h("div", { style: { marginTop: 10, padding: "8px 10px", borderRadius: 10, border: `1px solid ${alpha(tone, ".35")}`, background: alpha(tone, ".07") } },
1061
1116
  h("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, fontWeight: 600, color: textTone(tone) } },
@@ -1064,7 +1119,9 @@ window.__ModuleLoader__.load({
1064
1119
  h("span", { style: { fontWeight: 500, opacity: 0.9 } }, `${pool.available}/${pool.total}`)),
1065
1120
  h("p", { role: pool.exhausted ? "alert" : "status", style: { margin: "3px 0 0", fontSize: 11, color: textTone(tone) } }, summary),
1066
1121
  pool.unusableCount > 0 ? h("p", { style: { margin: "2px 0 0", fontSize: 10.5, opacity: 0.7 } }, t("ccPoolUnusable").replace("{unusable}", pool.unusableCount)) : null,
1067
- h("div", { style: { marginTop: 4 } }, pool.accounts.map(rowFor)));
1122
+ h("details", { style: { marginTop: 4 } },
1123
+ h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } }, t("ccPoolDetails")),
1124
+ h("div", { style: { maxHeight: 240, overflowY: "auto", marginTop: 4 } }, pool.accounts.map(rowFor))));
1068
1125
  }
1069
1126
 
1070
1127
  // ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
@@ -1126,18 +1183,31 @@ window.__ModuleLoader__.load({
1126
1183
  // 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时主文案仍是失败原因;
1127
1184
  // 例外是已过期——它本身就是「请重新登录」的行动项。
1128
1185
  const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || expiry.expired);
1129
- const windowName = (w) => t(KIND_LABEL[w.kind] ?? "wall");
1186
+ const windowName = (w) => w.budgetScope ? t({ key: "budgetKey", user: "budgetUser", team: "budgetTeam", scope: "budgetScope" }[w.budgetScope] ?? "budgetScope") : t(KIND_LABEL[w.kind] ?? "wall") + (w.detail?.scope === "all" ? ` · ${t("scopeAllShort")}` : "");
1130
1187
  const windowShort = (w) => multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
1131
1188
  const windowSub = (w) => {
1132
1189
  const head = t(multi ? "statusWorst" : "statusOnly").replace("{w}", windowName(w)).replace("{p}", String(windowLeft(w)));
1133
1190
  const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
1134
1191
  return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
1135
1192
  };
1136
- const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
1193
+ const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : providerId === "deepseek-account" ? t("pillNeedDeepseekAccount") : t("pillNeedKey"))
1137
1194
  : state === "no-cookie" ? t("pillNeedCookie") : null;
1138
1195
  const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
1139
1196
  if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
1140
1197
  if (state === "disabled") return { ...base, key: "disabled", color: NEUTRAL, icon: "–", title: t("statusDisabled"), sub: "", short: t("statusDisabled"), tinted: false };
1198
+ const pool = rawEntry?.accountPool;
1199
+ if (pool?.mode === "auto" && pool.available > 0) return { ...base, key: "pool", color: pool.blockedCount || pool.unusableCount ? "#eab308" : "#22c55e", icon: "✓", title: t("statusPoolAvailable"), sub: `${pool.available}/${pool.total}`, short: pool.blockedCount || pool.unusableCount ? `${pool.available}/${pool.total} ${t("statusPoolAvailableShort")}` : "", tinted: !!(pool.blockedCount || pool.unusableCount) };
1200
+ if (pool?.mode === "auto" && !pool.exhausted && pool.accounts?.some(a => a.state === "error" || a.availability === "unknown")) return { ...base, key: "pool-unknown", color: "#eab308", icon: "?", title: t("statusPoolUnknown"), sub: "", short: t("statusPoolUnknown"), tinted: true };
1201
+ const freshAdvice = rawEntry?.state === "ok" && rawEntry.freshness !== "stale";
1202
+ if (freshAdvice && rawEntry.availability === "blocked" && (rawEntry.availabilityReason !== "quota-exhausted" || !worstTier || worstTier.rank < 4)) return { ...base, key: "restricted", color: "#ef4444", icon: "✕", title: t("statusRestricted"), sub: "", short: t("statusRestrictedShort"), tinted: true };
1203
+ if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
1204
+ const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
1205
+ const wallet = !windows.length && rawEntry.coverage === "complete" && rawEntry.availability === "paygo" ? balanceVisual(rawEntry, t) : null;
1206
+ if (wallet && !expiryActionable) return { ...base, key: "paygo", color: wallet.color, icon: "", title: balanceText(rawEntry, t), sub: [title, wallet.label].filter(Boolean).join(" · "), short: wallet.value, tinted: wallet.tinted };
1207
+ return { ...base, key: rawEntry.availability, color: "#eab308", icon: rawEntry.availability === "unknown" ? "?" : "ℹ", title, sub: rawEntry.availability === "paygo" ? t("statusPaygoSub") : worst ? windowSub(worst) : "", short: t(rawEntry.availability === "paygo" ? "statusPaygoShort" : rawEntry.availability === "included" ? "statusIncludedShort" : "statusUnknownShort"), tinted: true };
1208
+ }
1209
+ const scopeLimited = worstTier?.rank >= 4 && windows.some(w => w.status !== "rate-limited" && w.detail?.scope && w.detail.scope !== "all") && windows.filter(w => w.status === "rate-limited").every(w => w.detail?.scope && w.detail.scope !== "all");
1210
+ if (scopeLimited) return { ...base, key: "scope", color: "#eab308", icon: "⚠", title: t("statusScopeLimited"), sub: windowSub(worst), short: t("statusScopeShort"), tinted: true };
1141
1211
  if (worstTier && worstTier.rank >= 4) {
1142
1212
  const title = t(worst.cascade ? "statusBlocked" : "statusExhausted");
1143
1213
  return { ...base, key: "blocked", color: worstTier.color, icon: "✕", title, sub: windowSub(worst), short: windowShort(worst) + t(worst.cascade ? "pillBlocked" : "pillLimited"), tinted: true };
@@ -1156,7 +1226,7 @@ window.__ModuleLoader__.load({
1156
1226
  // 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
1157
1227
  const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
1158
1228
  const worse = worstTier && worstTier.rank >= 3;
1159
- 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 };
1229
+ return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: [!worst ? balanceVisual(entry, t)?.value : null, label].filter(Boolean).join(" · "), tinted: true };
1160
1230
  }
1161
1231
  if (worstTier) {
1162
1232
  const sub = windowSub(worst);
@@ -1164,9 +1234,10 @@ window.__ModuleLoader__.load({
1164
1234
  if (worstTier.rank === 2) return { ...base, key: "low", color: worstTier.color, icon: "⚠", title: t("statusLow"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
1165
1235
  return { ...base, key: "ok", color: worstTier.color, icon: "✓", title: t("statusPlenty"), sub, short: "", tinted: false };
1166
1236
  }
1237
+ if (entry.extras?.some(e => e.kind === "quota-status" && e.value === "unlimited")) return { ...base, key: "unlimited", color: NEUTRAL, icon: "ℹ", title: t("statusUnlimited"), sub: "", short: t("statusUnlimited"), tinted: false };
1167
1238
  // 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
1168
- const balance = balanceText(entry, t);
1169
- return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
1239
+ const balance = balanceText(entry, t), visual = balanceVisual(entry, t);
1240
+ return { ...base, key: "balance", color: visual?.color ?? NEUTRAL, icon: visual ? "" : "ℹ", title: balance ?? t("noUsageData"), sub: visual?.label ?? "", short: visual?.value ?? balance ?? t("noUsageData"), tinted: visual?.tinted ?? false };
1170
1241
  }
1171
1242
  /** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
1172
1243
  function statusBanner(status) {
@@ -1218,7 +1289,7 @@ window.__ModuleLoader__.load({
1218
1289
  style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
1219
1290
  },
1220
1291
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1221
- h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, t(KIND_LABEL[w.kind] ?? "wall")),
1292
+ h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, w.budgetScope ? t({ key: "budgetKey", user: "budgetUser", team: "budgetTeam", scope: "budgetScope" }[w.budgetScope] ?? "budgetScope") : t(KIND_LABEL[w.kind] ?? "wall") + (w.detail?.scope === "all" ? ` · ${t("scopeAllShort")}` : "")),
1222
1293
  alert ? statusChip(t(tier.badge), tier.color) : null,
1223
1294
  h("span", { style: { flex: 1 } }),
1224
1295
  h("strong", { style: { fontSize: 18, lineHeight: 1.1, color: alert ? textTone(tier.color) : "inherit", whiteSpace: "nowrap" } }, `${left}%`),
@@ -1239,7 +1310,7 @@ window.__ModuleLoader__.load({
1239
1310
  * 弹层与设置页共用,避免只有其中一处带分组。
1240
1311
  */
1241
1312
  function UsageWindowList({ windows, t, getLocale, uncertain = false }) {
1242
- return (windows || []).map((w, index) => h(react.Fragment, { key: w.kind },
1313
+ return (windows || []).map((w, index) => h(react.Fragment, { key: `${w.budgetScope ?? w.kind}:${index}` },
1243
1314
  w.groupLabel && w.groupLabel !== windows[index - 1]?.groupLabel
1244
1315
  ? h("p", { role: "note", style: { margin: index ? "14px 0 0" : "6px 0 0", fontSize: 11, opacity: 0.6 } }, w.groupLabel)
1245
1316
  : null,
@@ -1338,10 +1409,10 @@ window.__ModuleLoader__.load({
1338
1409
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1339
1410
  // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
1340
1411
  // 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
1341
- 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 : ""}`;
1412
+ const tip = `${label}: ${st.title}${st.icon === "" && st.sub ? " · " + st.sub : ""}${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 : ""}`;
1342
1413
  // 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
1343
1414
  // 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
1344
- const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
1415
+ const glyph = windows.length ? miniBars(windows, degraded) : st.icon === "" ? null : statusGlyph(st.color, st.icon, 16);
1345
1416
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
1346
1417
  h("button", {
1347
1418
  type: "button",
@@ -1354,7 +1425,7 @@ window.__ModuleLoader__.load({
1354
1425
  display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
1355
1426
  borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
1356
1427
  background: st.tinted ? alpha(st.color, ".10") : "transparent",
1357
- color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
1428
+ color: st.icon === "" && st.tinted ? `color-mix(in srgb, ${st.color} 65%, currentColor)` : "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
1358
1429
  }
1359
1430
  },
1360
1431
  glyph,
@@ -1670,7 +1741,7 @@ window.__ModuleLoader__.load({
1670
1741
  const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
1671
1742
  : tab === "openai-codex" ? text("来自 $CODEX_HOME/auth.json 或 ~/.codex/auth.json(codex login)", "From $CODEX_HOME/auth.json or ~/.codex/auth.json (codex login)")
1672
1743
  : text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)");
1673
- const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel };
1744
+ const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel, "account-service": text("本机 DeepSeek 账号余额", "Local DeepSeek account balance") };
1674
1745
  const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1675
1746
  draft[kind + "Action"] !== "replace" ? h("button", { type: "button", style: btnStyle, disabled: kind === "cookie" && loginActive, onClick: () => editDraft({ [kind + "Action"]: "replace", [kind]: "", ...(kind === "key" ? { keyMode: "manual" } : {}) }) }, text(kind === "cookie" ? "手动导入" : "更换", kind === "cookie" ? "Manual import" : "Change")) : null,
1676
1747
  hasSaved && draft[kind + "Action"] !== "clear" ? h("button", { type: "button", style: btnStyle, disabled: kind === "cookie" && loginActive, onClick: () => { editDraft({ [kind + "Action"]: "clear", [kind]: "" }); setNotice({ ok: false, text: text("待清除:保存设置后将删除已保存的敏感凭据;取消编辑可保留。", "Pending removal: saving will delete the saved secret. Cancel edits to keep it.") }); } }, text(kind === "cookie" ? "清除登录凭据" : "清除", kind === "cookie" ? "Clear login credentials" : "Clear")) : null,
@@ -1686,10 +1757,11 @@ window.__ModuleLoader__.load({
1686
1757
  h("div", { style: { marginTop: 10 } },
1687
1758
  h("fieldset", { disabled: busy || !result.settings || typeof result.settings.revision !== "string", style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
1688
1759
  tab === "zai-coding" ? h("p", null, text("使用 Z.ai 国际站 Coding Plan Key;与中国站凭据独立。", "Use an international Z.ai Coding Plan key; credentials are separate from the China region.")) : null,
1760
+ tab === "deepseek" ? h("p", null, text("此入口只读取独立 API Key 的官方余额,不使用 DSH 账号或 OAuth。可继承 DEEPSEEK_API_KEY,或手动填写官方开放平台 API Key;余额查询不要求配置模型路由。", "This entry reads only the independent API-key balance, never the DSH account or OAuth. Inherit DEEPSEEK_API_KEY or enter an official platform API key; no model route is required.")) : null,
1689
1761
  tab === "synthetic" ? h("p", null, text("使用 Synthetic API Key,仅显示模型订阅请求额度。", "Use a Synthetic API key. Shows the model subscription request quota.")) : null,
1690
1762
  tab === "nanogpt" ? h("p", null, text("可用 NanoGPT API Key,推荐手动填写 Usage only 管理令牌(sk-nano-mgmt-…)。显示日/周输入 Token 与试用额度,图像额度单独计算,不纳入此处。", "Use a NanoGPT API key or, preferably, a manual Usage only management token (sk-nano-mgmt-…). Shows daily/weekly input tokens and trial quotas; image quotas are separate."), " ", h("a", { href: "https://nano-gpt.com/settings", target: "_blank", rel: "noopener noreferrer" }, text("打开官方设置", "Open official settings"))) : null,
1691
1763
  h("p", null, `${result.configured && result.configured[tab] ? t("detected") : t("notDetected")} · ${sources[source] || sources.none}`),
1692
- tab === "commandcode" ? h(react.Fragment, null, h("p", null, t("commandcodeManaged"))) : tab === "xai-oauth" ? h(react.Fragment, null, h("p", null, t("grokManaged"))) : tab === "openai-codex" ? h(react.Fragment, null,
1764
+ tab === "deepseek-account" ? h("p", null, text("只读 DSH「账号与余额」中已登录账号的充值余额。默认开启;不读取、导出或保存 OAuth token,不使用独立 API Key。请在 DSH 的账号与余额页面登录。", "Reads the signed-in account's recharge balance from DSH Account & balance. Enabled by default; never reads, exports or stores OAuth tokens, and never uses an independent API key. Sign in on DSH Account & balance.")) : tab === "commandcode" ? h(react.Fragment, null, h("p", null, t("commandcodeManaged"))) : tab === "xai-oauth" ? h(react.Fragment, null, h("p", null, t("grokManaged"))) : tab === "openai-codex" ? h(react.Fragment, null,
1693
1765
  h("p", null, text("只读 Codex CLI 的 ChatGPT 订阅登录($CODEX_HOME/auth.json 或 ~/.codex/auth.json):本页不保存凭据、不刷新 token。请用 codex login 登录;API Key 模式(auth_mode 不是 chatgpt)取不到订阅额度。", "Reads the Codex CLI ChatGPT subscription login ($CODEX_HOME/auth.json or ~/.codex/auth.json) read-only: this page never saves credentials or refreshes tokens. Sign in with codex login; API-key mode (auth_mode not chatgpt) cannot read subscription quota."))) : PROVIDER_META[tab].volc ? h(react.Fragment, null,
1694
1766
  h("p", null, text("额度查询要用火山引擎 IAM 的 Access Key(AK/SK 配对,权限建议只给方舟只读),与推理用的方舟 API Key 不是同一套;Ark 的这几条路由共用这一组凭据。", "Quota lookup needs a Volcengine IAM Access Key pair (Ark read-only is enough). This is NOT the Ark inference API key; every Ark route here shares this pair.")),
1695
1767
  // 用户通常已经登录着控制台,直接给入口比让他自己翻菜单快得多。
@@ -1757,7 +1829,7 @@ window.__ModuleLoader__.load({
1757
1829
  return h("div", { key: id, style: { gridColumn: editing ? "1 / -1" : undefined, display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", border: "1px solid rgba(128,128,128,.25)", borderRadius: 8, padding: 8 } },
1758
1830
  h("span", { style: { flex: 1, minWidth: 80 } }, title),
1759
1831
  h("button", { type: "button", role: "switch", "aria-checked": enabled, disabled: busy || dirty || !result.settings?.revision, "aria-label": `${text("启用", "Enable")} ${title}`, style: enabled ? primary : btnStyle, onClick: () => saveVisibility(isArk ? { ark: !enabled } : { providers: { [id]: !enabled } }) }, enabled ? text("已开启", "On") : text("已关闭", "Off")),
1760
- h("span", { style: { width: "100%", fontSize: 12, opacity: .75 } }, arkNote ? arkNote + " · " : "", !enabled ? text("检测已关闭", "Detection disabled") : !providerRoutable(result, id) ? text("本机没装这条路由:提供方插件或 CLI 没有注册它,开了也读不到东西", "Route not installed on this machine: no provider plugin or CLI registered it, so enabling it reads nothing") : e?.apiDetected === false || ["no-key", "no-cookie"].includes(e?.state) ? text(hideWithoutApi ? "未检测到 API · 默认隐藏" : "未检测到 API", "No API credentials detected") : e?.apiDetected === true || (e?.keySource && e.keySource !== "none") ? text("已检测到 API / 凭据", "API credentials detected") : text("待检测", "Detection pending")),
1832
+ h("span", { style: { width: "100%", fontSize: 12, opacity: .75 } }, arkNote ? arkNote + " · " : "", !enabled ? text("检测已关闭", "Detection disabled") : !providerCanQuery(result, id) ? text("本机没装这条路由:提供方插件或 CLI 没有注册它,开了也读不到东西", "Route not installed on this machine: no provider plugin or CLI registered it, so enabling it reads nothing") : e?.apiDetected === false || ["no-key", "no-cookie"].includes(e?.state) ? text(hideWithoutApi ? "未检测到 API · 默认隐藏" : "未检测到 API", "No API credentials detected") : e?.apiDetected === true || (e?.keySource && e.keySource !== "none") ? text("已检测到 API / 凭据", "API credentials detected") : text("待检测", "Detection pending")),
1761
1833
  h("details", { id: `subusage-credentials-${id}`, open: editing, style: { width: "100%" } },
1762
1834
  h("summary", { "aria-label": `${text("连接与凭据", "Connection & credentials")} ${PROVIDER_META[id].short}`, style: { cursor: "pointer" }, onClick: event => { event.preventDefault(); if (busy) return; if (editing) setEditorOpen(false); else openEditor(id); } }, text(id === "xiaomi-token-plan-cn" ? "登录与凭据" : PROVIDER_META[id].managedByPlugin ? "凭据来源" : "连接与凭据", id === "xiaomi-token-plan-cn" ? "Login & credentials" : PROVIDER_META[id].managedByPlugin ? "Credential source" : "Connection & credentials")),
1763
1835
  tab === id ? credentialEditor : null));