dsh-subusage 0.8.3 → 0.10.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
@@ -63,6 +63,7 @@ window.__ModuleLoader__.load({
63
63
  unavailable: "暂不可用",
64
64
  planLabel: "套餐",
65
65
  balanceLabel: "余额",
66
+ spendLabel: "已用",
66
67
  monthlyBalanceLabel: "月额度剩余",
67
68
  planSnapshotLimit: "月总额来自套餐快照,不含已购/赠送余额;未知套餐不推算上限",
68
69
  w5h: "5 小时",
@@ -104,9 +105,12 @@ window.__ModuleLoader__.load({
104
105
  noUsageData: "暂无额度数据",
105
106
  providerUnavailable: "用量暂不可用",
106
107
  pillNeedCommandcode: "需配置 —— 可在 设置 → Command Code 登录、运行 cmd login,或导出 COMMANDCODE_API_KEY 后重试。",
108
+ pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
107
109
  accountLabel: "账户",
108
110
  commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
109
111
  commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示药丸弹层中选择的账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。",
112
+ grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页只读、不保存凭据、不刷新 token。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
113
+ grokHelp: "并行读取 /v1/billing?format=credits 与 /v1/settings。creditUsagePercent 是统一用量池的已用百分比(通常为周池,重置于 currentPeriod.end,剩余 = 100 − 已用);套餐名取 subscription_tier_display;已购加量余额 prepaidBalance 以美元显示。API Key(XAI_API_KEY)路线取不到订阅周池,必须 OAuth 登录。",
110
114
  ccAccountsTitle: "Command Code 账户",
111
115
  ccAccountAuto: "自动轮换",
112
116
  ccAccountDefault: "默认账户",
@@ -163,6 +167,7 @@ window.__ModuleLoader__.load({
163
167
  unavailable: "Unavailable",
164
168
  planLabel: "Plan",
165
169
  balanceLabel: "Balance",
170
+ spendLabel: "Spent",
166
171
  monthlyBalanceLabel: "Monthly credits remaining",
167
172
  planSnapshotLimit: "Monthly total uses a plan snapshot, excluding purchased/free credits; unknown plans have no estimated cap",
168
173
  w5h: "5 hours",
@@ -204,9 +209,12 @@ window.__ModuleLoader__.load({
204
209
  noUsageData: "No quota data",
205
210
  providerUnavailable: "Usage unavailable",
206
211
  pillNeedCommandcode: "Setup required — sign in under Settings → Command Code, run cmd login, or export COMMANDCODE_API_KEY and retry.",
212
+ pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
207
213
  accountLabel: "Account",
208
214
  commandcodeManaged: "Credentials are managed by the Command Code provider plugin (Settings → Command Code); this page is read-only and stores nothing. Usage is fetched directly by this plugin from its billing endpoint, using the same credential sources (credential service COMMANDCODE_API_KEY → launch environment → ~/.commandcode/auth.json).",
209
215
  commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable. Shows the quota of the account selected in the pill popover (the default/top-level key account when none is selected), which may differ from the serving account; a custom apiBase in the provider plugin is not followed.",
216
+ grokManaged: "Credentials are the OAuth login file shared by dsh-grok-kit / Grok CLI (~/.grok/auth.json); this page is read-only — it stores nothing and never refreshes the token. Usage is fetched directly by this plugin from the Grok CLI billing proxy (cli-chat-proxy.grok.com) with the same credential.",
217
+ grokHelp: "Parallel requests to /v1/billing?format=credits and /v1/settings. creditUsagePercent is the USED percentage of the unified usage pool (usually weekly, resetting at currentPeriod.end; remaining = 100 − used). The plan name comes from subscription_tier_display; the purchased top-up balance prepaidBalance is shown in USD. An API key (XAI_API_KEY) cannot read the subscription pool — OAuth sign-in is required.",
210
218
  ccAccountsTitle: "Command Code accounts",
211
219
  ccAccountAuto: "Automatic (rotation order)",
212
220
  ccAccountDefault: "Default account",
@@ -222,6 +230,8 @@ window.__ModuleLoader__.load({
222
230
  const KIND_LABEL = { "5h": "w5h", "7d": "w7d", day: "wday", week: "wweek", month: "wmonth", period: "wperiod", sub: "wsub", rolling: "wrolling" };
223
231
  const KIND_SHORT = { "5h": "5h", "7d": "7d", day: "D", week: "W", month: "M", period: "P", sub: "Sub", rolling: "R" };
224
232
  const PROVIDER_META = {
233
+ // 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
234
+ "deepseek": { label: "DeepSeek", short: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
225
235
  "zai-coding": { label: "Z.ai Coding (International)", short: "Z.ai Intl", envName: "ZAI_CODING_API_KEY", defaultEnabled: false },
226
236
  "synthetic": { label: "Synthetic", short: "Synthetic", envName: "SYNTHETIC_API_KEY", defaultEnabled: false },
227
237
  "nanogpt": { label: "NanoGPT", short: "NanoGPT", envName: "NANOGPT_API_KEY", defaultEnabled: false },
@@ -231,10 +241,46 @@ window.__ModuleLoader__.load({
231
241
  "opencode-go": { label: "OpenCode Go", short: "OpenCode Go", envName: "OPENCODE_API_KEY" },
232
242
  // 凭据与多账户由提供方插件管理;managedByPlugin 时不渲染本地凭据编辑。
233
243
  "commandcode": { label: "Command Code", short: "Command", envName: "COMMANDCODE_API_KEY", managedByPlugin: true },
234
- "minimax": { label: "MiniMax (International)", short: "MiniMax", envName: "MINIMAX_API_KEY" },
235
- "minimax-cn": { label: "MiniMax (CN)", short: "MiniMax CN", envName: "MINIMAX_CN_API_KEY" }
244
+ // SuperGrok / X Premium 订阅(dsh-grok-kit 的 xai-oauth 路由):OAuth 登录文件凭据,无继承变量。
245
+ "xai-oauth": { label: "SuperGrok", short: "SuperGrok", envName: "", managedByPlugin: true },
246
+ // Codex / ChatGPT 订阅:凭据是 Codex CLI 的 OAuth 登录文件,只读、不刷新、不接受本地 Key。
247
+ // 默认开启:它对齐的是 DSH 内置的 openai-codex 路由,没登录时会被「默认隐藏」自动收起。
248
+ "openai-codex": { label: "Codex (ChatGPT)", short: "Codex", envName: "", managedByPlugin: true },
249
+ // 国际版默认关闭,中国版保持默认开启。
250
+ "minimax": { label: "MiniMax (International)", short: "MiniMax", envName: "MINIMAX_API_KEY", defaultEnabled: false },
251
+ "minimax-cn": { label: "MiniMax (CN)", short: "MiniMax CN", envName: "MINIMAX_CN_API_KEY" },
252
+ // 火山方舟:provider id 必须与官方插件 @volcengine/ark-plan-api 注册的路由逐字一致,
253
+ // 否则选中方舟模型时药丸不会出现。凭据是共享的 IAM AK/SK(volc),与这里的推理
254
+ // envName 是两套:推理 Key 查不到额度。
255
+ // arkcli(官方 CLI)安装的路由:id 由 `arkcli helper` 写成 arkcli-<planType>,
256
+ // 官方 CLI 是当前最主流的安装方式,故默认开启。
257
+ "arkcli-agent-plan": { label: "ARK Agent Plan (arkcli)", short: "ARK Plan", envName: "ARKCLI_AGENT_PLAN_API_KEY", volc: true },
258
+ "arkcli-coding-plan": { label: "ARK Coding Plan (arkcli)", short: "ARK Code", envName: "ARKCLI_CODING_PLAN_API_KEY", volc: true },
259
+ // 企业版/团队版:额度按席位下发,需 AK/SK 有查看席位的权限。
260
+ "arkcli-agent-plan-team": { label: "ARK Agent Plan Team (arkcli)", short: "ARK Plan Team", envName: "ARKCLI_AGENT_PLAN_TEAM_API_KEY", volc: true, defaultEnabled: false },
261
+ "arkcli-coding-plan-team": { label: "ARK Coding Plan Team (arkcli)", short: "ARK Code Team", envName: "ARKCLI_CODING_PLAN_TEAM_API_KEY", volc: true, defaultEnabled: false },
262
+ // 旧插件 @volcengine/ark-plan-api 的路由(保留兼容);short 与上面区分开,避免标签重名。
263
+ "ark-coding-plan-cn": { label: "Ark Coding Plan (CN, legacy plugin)", short: "Ark Coding", envName: "ARK_CODING_PLAN_CN_API_KEY", volc: true, defaultEnabled: false },
264
+ "ark-agent-plan-cn": { label: "Ark Agent Plan (CN, legacy plugin)", short: "Ark Agent", envName: "ARK_AGENT_PLAN_CN_API_KEY", volc: true, defaultEnabled: false },
265
+ "ark-coding-plan-byteplus": { label: "Ark Coding Plan (BytePlus, legacy plugin)", short: "Ark BytePlus", envName: "ARK_CODING_PLAN_BYTEPLUS_API_KEY", volc: true, defaultEnabled: false },
266
+ // 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
267
+ "siliconflow": { label: "SiliconFlow", short: "SiliconFlow", envName: "SILICONFLOW_API_KEY", defaultEnabled: false },
268
+ // 余额端点只对 management / provisioning key 开放;普通 key 只显示限额与用量。
269
+ "openrouter": { label: "OpenRouter", short: "OpenRouter", envName: "OPENROUTER_API_KEY", defaultEnabled: false },
270
+ // 余额型聚合商:单体 GET + 一个余额数字,药丸用余额兜底文案显示。
271
+ "novita": { label: "Novita AI", short: "Novita", envName: "NOVITA_API_KEY", defaultEnabled: false },
272
+ "hyperbolic": { label: "Hyperbolic", short: "Hyperbolic", envName: "HYPERBOLIC_API_KEY", defaultEnabled: false },
273
+ "deepinfra": { label: "DeepInfra", short: "DeepInfra", envName: "DEEPINFRA_API_KEY", defaultEnabled: false },
274
+ "chutes": { label: "Chutes", short: "Chutes", envName: "CHUTES_API_KEY", defaultEnabled: false },
275
+ "ollama-cloud": { label: "Ollama Cloud", short: "Ollama", envName: "OLLAMA_API_KEY", defaultEnabled: false },
276
+ "vercel-ai-gateway": { label: "Vercel AI Gateway", short: "Vercel AI", envName: "AI_GATEWAY_API_KEY", defaultEnabled: false },
277
+ // 额度端点只认 Management API Key,所以独立变量名。
278
+ "zenmux": { label: "ZenMux", short: "ZenMux", envName: "ZENMUX_MANAGEMENT_API_KEY", defaultEnabled: false },
279
+ // LiteLLM 是自建网关:除虚拟 Key 外还要填自己的 proxy 地址。
280
+ "litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
236
281
  };
237
- const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt"];
282
+ // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
283
+ 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", "ark-coding-plan-cn", "ark-agent-plan-cn", "ark-coding-plan-byteplus", "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"];
238
284
  function providerEnabled(settings, id) { return settings?.visibility?.providers?.[id] ?? (PROVIDER_META[id]?.defaultEnabled !== false); }
239
285
  function providerVisible(settings, id, entry) {
240
286
  if (!providerEnabled(settings, id)) return false;
@@ -302,7 +348,12 @@ window.__ModuleLoader__.load({
302
348
  }
303
349
  /** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
304
350
  function extraLabel(x, t) {
305
- return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : t("balanceLabel");
351
+ return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : x.kind === "spend" ? t("spendLabel") : t("balanceLabel");
352
+ }
353
+ /** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
354
+ function balanceText(entry, t) {
355
+ const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") : undefined;
356
+ return row ? `${t("balanceLabel")} ${row.value}` : null;
306
357
  }
307
358
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
308
359
  function durationText(ms, t) {
@@ -508,6 +559,39 @@ window.__ModuleLoader__.load({
508
559
  if (entry.coverage === "partial") return t("statusPartial");
509
560
  return t("statusOk");
510
561
  }
562
+ /**
563
+ * 需要用户行动时的内联入口类型。设置页的「状态」在上(用量面板)、「操作」在下(提供商管理),
564
+ * 出问题时用户得先去下面找到对应厂商才能修——这里让问题旁边直接出现修复动作。
565
+ * 正常状态返回 null:不打扰,也不重复管理区那份编辑器。
566
+ */
567
+ function usageActionKind(entry, providerId, t) {
568
+ if (!entry || entry.state === "loading" || entry.state === "disabled") return null;
569
+ const authFailed = /auth|credential/i.test(entry.errorCode || "");
570
+ if (providerId === "xiaomi-token-plan-cn") {
571
+ const expiry = cookieExpiry(entry, t);
572
+ if (expiry && expiry.expired) return "mimo-relogin";
573
+ // rank ≥ 2 才是「临近到期」(与药丸同一档位判定),否则正常倒计时不必打扰。
574
+ if (entry.state === "no-cookie" || authFailed || (expiry && expiry.rank >= 2)) return "mimo-login";
575
+ return null;
576
+ }
577
+ if (entry.state === "no-key" || entry.state === "no-cookie" || authFailed) return "credentials";
578
+ return null;
579
+ }
580
+ /**
581
+ * 待处理程度的排序权重:读取失败与 Cookie 过期最急,额度用尽与即将到期次之,数据不完整再次。
582
+ * **「未配置凭据」刻意不算**——那是用户自己的取舍(默认隐藏就是为收起它们),
583
+ * 把它顶到最前面会让每个还没配的厂商都来抢位置。
584
+ */
585
+ function usageAttentionRank(entry, t) {
586
+ if (!entry) return 0;
587
+ if (entry.state === "error") return 3;
588
+ const expiry = cookieExpiry(entry, t);
589
+ if (expiry && expiry.expired) return 3;
590
+ if ((entry.windows || []).some(w => w.status === "rate-limited")) return 2;
591
+ if (expiry && expiry.rank >= 2) return 2;
592
+ if (entry.freshness === "stale" || entry.coverage === "partial") return 1;
593
+ return 0;
594
+ }
511
595
  function providerDescription(entry, t) {
512
596
  const windows = canShowUsage(entry) ? entry.windows || [] : [];
513
597
  const worst = worstWindow(windows);
@@ -749,12 +833,21 @@ window.__ModuleLoader__.load({
749
833
  h("span", { style: { flex: 1 } }, labelText),
750
834
  ccState.activeId === id ? h("span", { style: { fontSize: 11, opacity: 0.75 } }, t("ccAccountActive")) : null
751
835
  );
752
- return h("div", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
753
- h("div", { style: { fontSize: 11, opacity: 0.7 } }, t("ccAccountsTitle")),
836
+ // 账户多时这一坨会把上方的额度顶出视野:折进 details,summary 只留「当前账户 + 数量」,
837
+ // 展开后列表自身限高滚动,弹层高度不随账户数增长。
838
+ const activeLabel = ccState.status !== "ready" ? null
839
+ : ccState.activeId === "" ? t("ccAccountAuto")
840
+ : ccState.activeId === "default" ? t("ccAccountDefault")
841
+ : (ccState.accounts.find(account => account.id === ccState.activeId)?.label ?? ccState.activeId);
842
+ const optionCount = ccState.status === "ready" ? ccState.accounts.length + 1 : 0;
843
+ return h("details", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
844
+ h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } },
845
+ `${t("ccAccountsTitle")}:${activeLabel ?? (ccState.status === "loading" || ccState.status === "idle" ? t("loading") : t("ccAccountsUnavailable"))}${optionCount > 1 ? `(${optionCount})` : ""}`),
754
846
  ccState.status === "ready"
755
847
  ? h("div", null,
756
- optionButton("", t("ccAccountAuto")),
757
- ccState.accounts.map((account) => optionButton(account.id, account.id === "default" ? t("ccAccountDefault") : account.label)),
848
+ h("div", { style: { maxHeight: 240, overflowY: "auto" } },
849
+ optionButton("", t("ccAccountAuto")),
850
+ ccState.accounts.map((account) => optionButton(account.id, account.id === "default" ? t("ccAccountDefault") : account.label))),
758
851
  ccState.error ? h("p", { style: { color: "#f87171", margin: "6px 0 0", whiteSpace: "pre-wrap" } }, `${t("ccSwitchFailed")}: ${ccState.error}`) : null,
759
852
  h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.65 } }, t("ccAccountsHint")))
760
853
  : h("p", { style: { margin: "4px 0 0", opacity: 0.7 } }, ccState.status === "loading" || ccState.status === "idle" ? t("loading") : t("ccAccountsUnavailable"))
@@ -886,11 +979,11 @@ window.__ModuleLoader__.load({
886
979
  const worst = windows.length > 0 ? worstWindow(windows) : null;
887
980
  const worstTier = worst ? usageTier(worst) : null;
888
981
  const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
889
- const summary = worst ? pillText(worst, windows.length > 1, t) : (entry ? t("noUsageData") : t("providerUnavailable"));
982
+ const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
890
983
  const stateText = !ok ? (snapshot && snapshot.entry ? snapshot.entry.state : failState) : void 0;
891
984
  const stateLabel = rawEntry ? statusText(rawEntry, t) : failState === "no-key" ? t("statusNoKey") : failState === "no-cookie" ? t("statusNoCookie") : failed ? t("statusError") : t("loading");
892
985
  // 配置缺失类失败给可行动指引,不倒裸报错。
893
- const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : t("pillNeedKey"))
986
+ const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
894
987
  : stateText === "no-cookie" ? t("pillNeedCookie")
895
988
  : null;
896
989
  const isConfig = stateText === "no-key" || stateText === "no-cookie";
@@ -907,7 +1000,7 @@ window.__ModuleLoader__.load({
907
1000
  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)" } : {};
908
1001
  const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
909
1002
  const triggerIcon = quotaBlocked ? worstTier.icon : expiryActionable ? expiry.icon : degraded ? "⚠" : worstTier ? worstTier.icon : ok ? "ℹ" : isConfig ? "⚙" : light;
910
- 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 + " · " : ""}${t("noUsageData")}` : isConfig ? t("needConfigShort") : stateLabel;
1003
+ 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;
911
1004
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
912
1005
  h("button", {
913
1006
  type: "button",
@@ -974,13 +1067,30 @@ window.__ModuleLoader__.load({
974
1067
  return PROVIDER_ORDER[0];
975
1068
  }
976
1069
  function draftFor(settings, id) {
977
- return { keyMode: settings && settings.keyModes && settings.keyModes[id] || "inherit", keyAction: "keep", key: "", cookieAction: "keep", cookie: "", zai: { type: 1, organization: "", project: "", ...(settings && settings.zai) } };
1070
+ return { keyMode: settings && settings.keyModes && settings.keyModes[id] || "inherit", keyAction: "keep", key: "", cookieAction: "keep", cookie: "", zai: { type: 1, organization: "", project: "", ...(settings && settings.zai) }, volc: { accessKey: "", secret: "", clear: false }, litellm: { baseUrl: settings && settings.litellm && settings.litellm.baseUrl || "" } };
978
1071
  }
979
1072
  function settingsPatch(id, draft, revision) {
980
1073
  if (typeof revision !== "string" || !revision) throw new Error("请先加载最新设置,再编辑保存 / Load settings before saving");
981
1074
  const patch = { providerId: id, expectedRevision: revision };
982
1075
  // 凭据由提供方插件管理的模型商没有任何本地凭据补丁可保存。
983
1076
  if (PROVIDER_META[id].managedByPlugin) return patch;
1077
+ if (PROVIDER_META[id].volc) {
1078
+ // AK/SK 是秘密且不回填:只有真正填了值的那一项才发 replace,避免「只改 AK 把 SK 抹掉」;
1079
+ // 清除必须显式点按钮,空输入不是清空指令。
1080
+ patch.keyMode = draft.keyMode;
1081
+ if (draft.volc.clear) patch.volc = { accessKeyId: { action: "clear" }, secretAccessKey: { action: "clear" } };
1082
+ else {
1083
+ const volc = {};
1084
+ const accessKey = draft.volc.accessKey.trim(), secret = draft.volc.secret.trim();
1085
+ for (const [field, value] of [["accessKeyId", accessKey], ["secretAccessKey", secret]]) {
1086
+ if (!value) continue;
1087
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error("Access Key 不能包含控制字符");
1088
+ volc[field] = { action: "replace", value };
1089
+ }
1090
+ if (Object.keys(volc).length) patch.volc = volc;
1091
+ }
1092
+ return patch;
1093
+ }
984
1094
  if (id === "xiaomi-token-plan-cn") {
985
1095
  patch.cookieUpdate = { action: draft.cookieAction };
986
1096
  if (draft.cookieAction === "replace") patch.cookieUpdate.value = validateCookieText(draft.cookie);
@@ -992,6 +1102,8 @@ window.__ModuleLoader__.load({
992
1102
  patch.keyUpdate.value = draft.key.trim();
993
1103
  }
994
1104
  if (id === "zai-coding-cn") patch.zai = { ...draft.zai };
1105
+ // LiteLLM 的代理地址不是秘密,原样提交;空串表示清空(请求时会按「未配置」提示)。
1106
+ if (id === "litellm") patch.litellm = { baseUrl: draft.litellm.baseUrl.trim() };
995
1107
  }
996
1108
  return patch;
997
1109
  }
@@ -1130,8 +1242,15 @@ window.__ModuleLoader__.load({
1130
1242
  const primary = { ...btnStyle, background: "rgba(59,130,246,.12)", borderColor: "rgba(59,130,246,.65)" };
1131
1243
  const entries = result.entries || [];
1132
1244
  const entry = entries.find((e) => e.providerId === tab);
1133
- const visibleIds = PROVIDER_ORDER.filter(id => providerVisible(result.settings, id, entries.find(e => e.providerId === id)));
1245
+ // 待处理的排前面;组内保持登记顺序,所以顺序不会自己抖动。
1246
+ const visibleIds = PROVIDER_ORDER
1247
+ .map((id, index) => ({ id, index, rank: usageAttentionRank(entries.find(e => e.providerId === id), t) }))
1248
+ .filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id)))
1249
+ .sort((a, b) => b.rank - a.rank || a.index - b.index)
1250
+ .map(item => item.id);
1134
1251
  const success = entries.filter((e) => visibleIds.includes(e.providerId) && e.state === "ok" && e.freshness !== "stale").length;
1252
+ // 顶部计数同时是「出问题的那几家在哪」的入口:点一下就切到第一家没读成功的。
1253
+ const troubledIds = visibleIds.filter(id => { const e = entries.find(item => item.providerId === id); return !(e && e.state === "ok" && e.freshness !== "stale"); });
1135
1254
  const hideWithoutApi = result.settings?.visibility?.hideWithoutApi !== false;
1136
1255
  react.useEffect(() => {
1137
1256
  if (visibleIds.length && !visibleIds.includes(usageTab)) setUsageTab(visibleIds[0]);
@@ -1139,6 +1258,15 @@ window.__ModuleLoader__.load({
1139
1258
  const usageId = visibleIds.includes(usageTab) ? usageTab : visibleIds[0];
1140
1259
  const usageEntry = entries.find(e => e.providerId === usageId);
1141
1260
  const usageMeta = PROVIDER_META[usageId];
1261
+ // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1262
+ const usageAction = usageActionKind(usageEntry, usageId, t);
1263
+ // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1264
+ const revealEditor = (target) => {
1265
+ openEditor(target);
1266
+ const box = root.current?.querySelector?.("#subusage-provider-management");
1267
+ if (box) box.open = true;
1268
+ setTimeout(() => { const el = root.current?.querySelector?.(`#subusage-credentials-${target}`); if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" }); }, 0);
1269
+ };
1142
1270
  react.useEffect(() => {
1143
1271
  if (!usageId) return;
1144
1272
  const deactivate = usageStore.activate(usageId);
@@ -1147,7 +1275,11 @@ window.__ModuleLoader__.load({
1147
1275
  }, [usageStore, usageId]);
1148
1276
  const meta = PROVIDER_META[tab];
1149
1277
  const source = entry && entry.keySource || "none";
1150
- 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": text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)") };
1278
+ // auth-file 有三家(Grok / Codex / Command Code),凭据来源文件各不相同,不能只特判一家。
1279
+ const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
1280
+ : tab === "openai-codex" ? text("来自 $CODEX_HOME/auth.json 或 ~/.codex/auth.json(codex login)", "From $CODEX_HOME/auth.json or ~/.codex/auth.json (codex login)")
1281
+ : text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)");
1282
+ 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 };
1151
1283
  const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1152
1284
  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,
1153
1285
  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,
@@ -1161,7 +1293,18 @@ window.__ModuleLoader__.load({
1161
1293
  tab === "synthetic" ? h("p", null, text("使用 Synthetic API Key,仅显示模型订阅请求额度。", "Use a Synthetic API key. Shows the model subscription request quota.")) : null,
1162
1294
  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,
1163
1295
  h("p", null, `${result.configured && result.configured[tab] ? t("detected") : t("notDetected")} · ${sources[source] || sources.none}`),
1164
- tab === "commandcode" ? h(react.Fragment, null, h("p", null, t("commandcodeManaged"))) : tab === "xiaomi-token-plan-cn" ? h(react.Fragment, null,
1296
+ 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,
1297
+ 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,
1298
+ 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.")),
1299
+ h("div", { role: "group", "aria-label": text("Key 来源模式", "Key source mode"), style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1300
+ ["inherit", "manual"].map((mode) => h("button", { key: mode, type: "button", "aria-pressed": draft.keyMode === mode, style: draft.keyMode === mode ? primary : btnStyle, onClick: () => editDraft({ keyMode: mode }) }, `${draft.keyMode === mode ? "✓ " : ""}${mode === "inherit" ? text("继承凭据 / 环境变量", "Inherit credentials / environment") : text("手动 AK/SK", "Manual AK/SK")}`))),
1301
+ h("p", { style: { fontSize: 12 } }, text("继承变量:", "Inherited variables: "), h("code", null, "VOLC_ACCESSKEY"), " / ", h("code", null, "VOLC_SECRETKEY")),
1302
+ h("p", { style: { fontSize: 12 } }, result.settings && result.settings.volc && result.settings.volc.hasAccessKeyId && result.settings.volc.hasSecretAccessKey ? text("已保存 AK 与 SK(不回填)", "AK and SK saved (never filled back)") : result.settings && result.settings.volc && (result.settings.volc.hasAccessKeyId || result.settings.volc.hasSecretAccessKey) ? text("只保存了一半:AK 与 SK 必须成对配置", "Only half saved: AK and SK must be configured together") : text("未保存 AK/SK", "No AK/SK saved")),
1303
+ (result.settings?.volc?.hasAccessKeyId || result.settings?.volc?.hasSecretAccessKey) && !draft.volc.clear ? h("button", { type: "button", style: btnStyle, disabled: busy, onClick: () => { editDraft({ volc: { accessKey: "", secret: "", clear: true } }); setNotice({ ok: false, text: text("待清除:保存后将删除已保存的 AK/SK;取消编辑可保留。", "Pending removal: saving deletes the saved AK/SK. Cancel edits to keep it.") }); } }, text("清除 AK/SK", "Clear AK/SK")) : null,
1304
+ draft.volc.clear ? h("span", { role: "status" }, text("将删除 AK/SK(尚未保存)", "AK/SK will be deleted (not saved yet)")) : null,
1305
+ h("label", null, "AccessKey ID", h("input", { autoComplete: "off", spellCheck: false, value: draft.volc.accessKey, disabled: busy, onChange: (e) => editDraft({ volc: { ...draft.volc, accessKey: e.target.value } }), style: inputStyle })),
1306
+ h("label", null, "SecretAccessKey", h("input", { type: "password", autoComplete: "new-password", value: draft.volc.secret, disabled: busy, onChange: (e) => editDraft({ volc: { ...draft.volc, secret: e.target.value } }), style: inputStyle })),
1307
+ h("p", { style: { fontSize: 12, opacity: .75 } }, text("留空表示不修改该项;AK 与 SK 必须来自同一个 IAM 用户,跨来源拼凑会被服务端判为签名无效。", "Leave a field empty to keep it. AK and SK must come from the same IAM user; mixed sources fail signature verification."))) : tab === "xiaomi-token-plan-cn" ? h(react.Fragment, null,
1165
1308
  h("button", { type: "button", style: primary, disabled: loginActive, onClick: startLogin }, text("登录并自动导入", "Sign in & import automatically")),
1166
1309
  loginActive ? h("div", { role: "status", "aria-live": "polite", style: { marginTop: 8 } },
1167
1310
  h("progress", { "aria-label": text("登录进度", "Login progress"), style: { width: 100, marginRight: 8 } }),
@@ -1183,12 +1326,15 @@ window.__ModuleLoader__.load({
1183
1326
  h("p", { style: { fontSize: 12 } }, result.settings && result.settings.hasKeys && result.settings.hasKeys[tab] ? text("已保存 Key(不回填)", "Key saved (never filled back)") : text("未保存手动 Key", "No manual key saved")),
1184
1327
  credentialButtons("key", result.settings && result.settings.hasKeys && result.settings.hasKeys[tab]),
1185
1328
  draft.keyAction === "replace" ? h("label", null, t("keyLabel"), h("input", { type: "password", autoComplete: "new-password", value: draft.key, disabled: busy, onChange: (e) => editDraft({ key: e.target.value }), style: inputStyle })) : null),
1329
+ tab === "litellm" ? h(react.Fragment, null,
1330
+ h("label", null, text("代理地址", "Proxy base URL"), h("input", { autoComplete: "off", spellCheck: false, value: draft.litellm.baseUrl, disabled: busy, placeholder: "https://litellm.example.com", onChange: (e) => editDraft({ litellm: { ...draft.litellm, baseUrl: e.target.value } }), style: inputStyle })),
1331
+ h("p", { style: { fontSize: 12, opacity: .75 } }, text("管理端点在 proxy 根:末尾的 /v1 会被自动去掉。虚拟 Key 与推理用的是同一把。", "Admin endpoints live at the proxy root: a trailing /v1 is stripped. The virtual key is the same one you use for inference."))) : null,
1186
1332
  tab === "zai-coding-cn" ? h(react.Fragment, null,
1187
1333
  h("div", { role: "radiogroup", "aria-label": t("zaiType"), style: { display: "flex", gap: 8, flexWrap: "wrap" } }, [1, 2].map(type => h("button", { key: type, type: "button", role: "radio", "aria-checked": draft.zai.type === type, style: draft.zai.type === type ? primary : btnStyle, disabled: busy, onClick: () => editDraft({ zai: { ...draft.zai, type } }) }, t(type === 1 ? "zaiTypePersonal" : "zaiTypeTeam")))),
1188
1334
  draft.zai.type === 2 ? ["organization", "project"].map((name) => h("label", { key: name }, t(name === "organization" ? "zaiOrg" : "zaiProject"), h("input", { value: draft.zai[name] || "", disabled: busy, style: inputStyle, onChange: (e) => editDraft({ zai: { ...draft.zai, [name]: e.target.value } }) }))) : null) : null,
1189
1335
  dirty ? h("div", { style: { display: "flex", gap: 8, marginTop: 10 } }, h("button", { type: "button", disabled: busy || (tab === "xiaomi-token-plan-cn" && loginActive), style: primary, onClick: onSave }, t(busy ? "saving" : "save")), h("button", { type: "button", disabled: busy, style: btnStyle, onClick: cancel }, text("取消编辑", "Cancel edits"))) : null)),
1190
1336
  h("details", { style: { marginTop: 12 } }, h("summary", { style: { cursor: "pointer" } }, text("技术信息与帮助", "Technical information & help")),
1191
- h("code", null, tab), tab !== "xiaomi-token-plan-cn" ? h("p", null, text("继承变量:", "Inherited variable: "), h("code", null, meta.envName)) : null, h("p", null, t("hint")), ["minimax", "minimax-cn"].includes(tab) ? h("p", null, text("需要对应区域的 Token Plan / Coding Plan 订阅 Key,不是按量计费 Key。读取通用/编程池的剩余百分比;零总计、无上限或未报告的窗口不会显示为零用量,周加量不折算为额外百分比。", "Use a Token Plan / Coding Plan subscription key for the selected region, not a pay-as-you-go key. Shows the general/coding pool; zero-total, unlimited or absent windows are not displayed as zero usage. Weekly boosts are not converted into extra percentage.")) : null, tab === "zai-coding-cn" ? h("p", null, t("zaiOrgHint")) : null, tab === "commandcode" ? h("p", null, t("commandcodeHelp")) : null,
1337
+ h("code", null, tab), tab !== "xiaomi-token-plan-cn" && meta.envName ? h("p", null, text("继承变量:", "Inherited variable: "), h("code", null, meta.envName)) : null, h("p", null, t("hint")), ["minimax", "minimax-cn"].includes(tab) ? h("p", null, text("需要对应区域的 Token Plan / Coding Plan 订阅 Key,不是按量计费 Key。读取通用/编程池的剩余百分比;零总计、无上限或未报告的窗口不会显示为零用量,周加量不折算为额外百分比。", "Use a Token Plan / Coding Plan subscription key for the selected region, not a pay-as-you-go key. Shows the general/coding pool; zero-total, unlimited or absent windows are not displayed as zero usage. Weekly boosts are not converted into extra percentage.")) : null, tab === "zai-coding-cn" ? h("p", null, t("zaiOrgHint")) : null, tab === "commandcode" ? h("p", null, t("commandcodeHelp")) : null, tab === "xai-oauth" ? h("p", null, t("grokHelp")) : null,
1192
1338
  entry && entry.lastAttemptAt ? h("p", null, `${t("lastUpdated")} ${new Date(entry.lastAttemptAt).toLocaleString(getLocale())}`) : null)
1193
1339
  );
1194
1340
  const providerManagement = h("details", { id: "subusage-provider-management", open: !visibleIds.length || undefined, "aria-label": text("提供商管理", "Provider management"), style: { border: "1px solid rgba(128,128,128,.3)", borderRadius: 10, padding: 12, marginTop: 16 } },
@@ -1205,7 +1351,7 @@ window.__ModuleLoader__.load({
1205
1351
  h("button", { type: "button", role: "switch", "aria-checked": enabled, disabled: busy || dirty || !result.settings?.revision, "aria-label": `${text("启用", "Enable")} ${PROVIDER_META[id].short}`, style: enabled ? primary : btnStyle, onClick: () => saveVisibility({ providers: { [id]: !enabled } }) }, enabled ? text("已开启", "On") : text("已关闭", "Off")),
1206
1352
  h("span", { style: { width: "100%", fontSize: 12, opacity: .75 } }, !enabled ? text("检测已关闭", "Detection disabled") : 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")),
1207
1353
  h("details", { id: `subusage-credentials-${id}`, open: editing, style: { width: "100%" } },
1208
- 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" ? "登录与凭据" : id === "commandcode" ? "凭据来源" : "连接与凭据", id === "xiaomi-token-plan-cn" ? "Login & credentials" : id === "commandcode" ? "Credential source" : "Connection & credentials")),
1354
+ 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")),
1209
1355
  tab === id ? credentialEditor : null));
1210
1356
  }))
1211
1357
  )
@@ -1213,7 +1359,14 @@ window.__ModuleLoader__.load({
1213
1359
  return h("div", { ref: root, style: { padding: 16, maxWidth: 720, minWidth: 0, font: "13px/1.6 sans-serif", containerType: "inline-size", containerName: "subusage" } },
1214
1360
  h("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" } },
1215
1361
  h("h2", { style: { margin: 0, fontSize: 16 } }, t("title")),
1216
- h("span", { title: text("仅表示数据获取成功,不表示还有额度", "Fetch success only; not remaining quota"), style: { opacity: .75 } }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
1362
+ h("button", {
1363
+ type: "button",
1364
+ disabled: !troubledIds.length,
1365
+ onClick: () => { if (troubledIds.length) chooseUsage(troubledIds[0]); },
1366
+ title: troubledIds.length ? text(`跳到 ${PROVIDER_META[troubledIds[0]].short}(共 ${troubledIds.length} 家待处理)`, `Jump to ${PROVIDER_META[troubledIds[0]].short} (${troubledIds.length} need attention)`) : text("全部获取成功", "All fetched"),
1367
+ "aria-label": text(`${success}/${visibleIds.length} 家数据获取成功;${troubledIds.length ? `点击跳到 ${PROVIDER_META[troubledIds[0]].short}` : "没有待处理项"}`, `${success}/${visibleIds.length} providers fetched; ${troubledIds.length ? `click to jump to ${PROVIDER_META[troubledIds[0]].short}` : "nothing needs attention"}`),
1368
+ style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
1369
+ }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
1217
1370
  h("button", { type: "button", disabled: busy || !usageId, style: primary, onClick: () => refresh(false) }, text("刷新当前", "Refresh current")),
1218
1371
  h("button", { type: "button", disabled: busy, style: btnStyle, onClick: () => refresh(true) }, text("刷新全部", "Refresh all"))
1219
1372
  ),
@@ -1239,6 +1392,10 @@ window.__ModuleLoader__.load({
1239
1392
  h("p", { role: "status", style: { margin: "4px 0", color: usageEntry && usageEntry.state !== "ok" ? "#dc2626" : "inherit" } }, statusText(usageEntry, t)),
1240
1393
  cacheAge(usageEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(usageEntry, t)) : null,
1241
1394
  usageEntry && usageEntry.error ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1395
+ 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)" } },
1396
+ 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.")),
1397
+ 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,
1398
+ h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1242
1399
  canShowUsage(usageEntry) && (usageEntry.windows || []).length ? (usageEntry.windows || []).map((w) => h(UsageWindowRow, { key: w.kind, w, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" })) : h("p", { style: { opacity: .75 } }, usageEntry ? t("noUsageData") : t("loading")),
1243
1400
  canShowUsage(usageEntry) && usageEntry.extras ? h("p", { style: { opacity: .75 } }, usageEntry.extras.map((x) => `${extraLabel(x, t)}: ${x.value}`).join(" · ")) : null,
1244
1401
  usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null