dsh-subusage 0.8.2 → 0.9.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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # dsh-subusage —— DeepSeek Harness 订阅用量显示
2
2
 
3
- 在模型选择器旁显示当前提供商的订阅余量,点击查看各周期用量、重置时间和套餐信息。设置页集中管理十个提供商/区域的检测开关与凭据。
3
+ 在模型选择器旁显示当前提供商的订阅余量,点击查看各周期用量、重置时间和套餐信息。设置页集中管理二十六个提供商/区域的检测开关与凭据。
4
4
 
5
- 支持 **Z.ai Coding(中国与国际)/ Kimi Coding / Xiaomi MiMo / OpenCode Go / Command Code / MiniMax(国际与中国)/ Synthetic / NanoGPT**。
5
+ 支持 **DeepSeek 官方(余额)/ Z.ai Coding(中国与国际)/ Kimi Coding / Xiaomi MiMo / OpenCode Go / Command Code / SuperGrok(xAI Grok OAuth 订阅)/ Codex(ChatGPT Plus/Pro/Team 订阅)/ MiniMax(国际与中国)/ Synthetic / NanoGPT(配额 + 余额)/ 火山方舟 Ark(Coding Plan 中国、Agent Plan 中国、Coding Plan BytePlus)/ SiliconFlow / OpenRouter / Novita / Hyperbolic / DeepInfra / Chutes / Ollama Cloud / Vercel AI Gateway / ZenMux / LiteLLM(自建网关)**。
6
6
 
7
7
  ## 安装与快速开始
8
8
 
9
- 当前仓库版本 **0.8.2**,可从 GitHub Release 获取。npm 自动发布采用 Trusted Publishing(GitHub OIDC);可用版本见 [npm](https://www.npmjs.com/package/dsh-subusage)。安装时使用目标 DSH 部署提供的插件管理器或 CLI,并启用本 bundle:
9
+ 当前仓库版本 **0.9.0**,可从 GitHub Release 获取。npm 自动发布采用 Trusted Publishing(GitHub OIDC);可用版本见 [npm](https://www.npmjs.com/package/dsh-subusage)。安装时使用目标 DSH 部署提供的插件管理器或 CLI,并启用本 bundle:
10
10
 
11
11
  | 来源 | 安装标识或下载地址 |
12
12
  |---|---|
@@ -20,7 +20,7 @@
20
20
 
21
21
  1. 安装并启用 bundle,按部署方式重载或重启,使 Host、Client 与依赖一起生效。
22
22
  2. 打开 **设置 → 订阅用量 → 提供商管理**,为需要的订阅配置凭据。
23
- 3. **Z.ai 国际、Synthetic、NanoGPT 默认关闭**,必须手动开启。原有七项默认开启;升级保留已保存的开关。
23
+ 3. **Z.ai 国际、Synthetic、NanoGPT、火山方舟 Ark 三家、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、MiniMax 国际版、ZenMux、LiteLLM**默认关闭,必须手动开启(共十七项)。其余九项默认开启;升级保留已保存的开关。
24
24
  4. 点击「刷新当前」检查结果;选中对应提供商的模型后,输入区显示余量药丸。
25
25
 
26
26
  「没有检测到API的默认隐藏」默认开启。首次没有可见条目时,管理区自动展开;关闭的提供商仍可配置凭据,但不会请求用量。插件不注册模型路由,药丸按提供商 ID 匹配,配置表见下文。
@@ -43,22 +43,52 @@
43
43
 
44
44
  顶部 `X/Y 家数据获取成功` 仅表示接口读取健康度。厂商图标/颜色表达额度与异常情况;例如读取成功后仍可能显示「已达限额」。未获取额度的部分数据不能视为可用。
45
45
 
46
+ ### 出错时直接在卡片上修
47
+
48
+ 设置页原本是「状态在上、操作在下」:用量面板告诉你 Cookie 已过期,但修复入口在底部收起的「提供商管理」里,要先展开、再找到那一家、再展开「连接与凭据」。现在用量面板会在**真正需要处理时**内联一条行动条:
49
+
50
+ | 情形 | 卡片上给出的动作 |
51
+ |---|---|
52
+ | MiMo 未登录 / Cookie 被拒 | 「登录并自动导入」+「手动填写」 |
53
+ | MiMo Cookie 已过期或 2 小时内到期 | 「重新登录」+「手动填写」 |
54
+ | 其它厂商缺凭据或凭据被拒 | 「配置凭据」——展开该厂商的编辑器并滚动过去 |
55
+
56
+ **正常状态不显示行动条**:还有几小时的倒计时不算事件,避免它变成常驻噪音。行动条只是「把入口送到眼前」,凭据编辑仍然复用下面那一份编辑器,不存在两套字段或两套校验。
57
+
58
+ 同一类问题的另外两处也一起处理了:
59
+
60
+ - **待处理的排到 tab 前面**:读取失败、Cookie 已过期最急,额度用尽与即将到期次之,数据不完整再次;**「未配置凭据」刻意不算**(那是用户自己的取舍,默认隐藏就是为收起它们),否则每个还没配的厂商都会来抢位置。同档内保持登记顺序,所以顺序不会自己抖动。
61
+ - **顶部「X/Y 家数据获取成功」是可点击的**:一下就切到第一家没读成功的;全部正常时按钮禁用,不做无意义的跳转。
62
+
63
+ ### 药丸里的 Command Code 账户区
64
+
65
+ 账户多的时候,账户列表会把上方的额度顶出视野。现在整块折进一个可展开区:标题行直接显示**当前账户与选项数量**,展开后的列表自身限高滚动(240px),弹层高度不再随账户数增长。降级状态(提供方插件未运行)同样折起,只留一行说明。
66
+
46
67
  ### 凭据与环境
47
68
 
48
69
  | 模型商 | 默认继承变量 | 说明 |
49
70
  |---|---|---|
71
+ | DeepSeek | `DEEPSEEK_API_KEY` | 余额型:`GET /user/balance` 返回账户余额,**与推理是同一把 Key** |
50
72
  | Z.ai | `ZAI_CODING_CN_API_KEY` | 团队套餐另填组织、项目 ID |
51
73
  | Z.ai 国际 | `ZAI_CODING_API_KEY` | `zai-coding`,默认关闭,独立国际 Coding Plan Key |
52
74
  | Synthetic | `SYNTHETIC_API_KEY` | `synthetic`,默认关闭,模型订阅请求额度 |
53
- | NanoGPT | `NANOGPT_API_KEY` | `nanogpt`,默认关闭;也可手动填写 Usage only 管理令牌 |
54
- | Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开平台 Key |
75
+ | NanoGPT | `NANOGPT_API_KEY` | `nanogpt`,默认关闭;也可手动填写 Usage only 管理令牌。配额之外还会读一次账户余额(余额端点用 `x-api-key` 而不是 Bearer;拿不到就静默降级,不影响配额) |
76
+ | ZenMux | `ZENMUX_MANAGEMENT_API_KEY` | 额度端点**只认 Management API Key**(推理 key 不适用),所以变量名单独区分。在 <https://zenmux.ai/platform/management> 创建该 key |
77
+ | LiteLLM | `LITELLM_API_KEY` + 代理地址 | 自建网关:除虚拟 Key(**与推理同一把**)还要在凭据区填写自己的 proxy 地址。管理端点在 **proxy 根**——填了 `/v1` 也会被去掉,不会拼成 `/v1/key/info` |
78
+ | Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开平台 Key;窗口集合按套餐体系下发(老套餐 5 小时 + 7 天,新套餐 Go / Plus 为 5 小时 + 月度) |
55
79
  | MiMo | 不使用 API Key | 通过控制台 Cookie 会话读取 |
56
80
  | OpenCode Go | `OPENCODE_API_KEY` | OpenCode Go Key |
57
81
  | MiniMax 国际 | `MINIMAX_API_KEY` | `minimax` 路由;国际站订阅 Key |
58
82
  | MiniMax 中国 | `MINIMAX_CN_API_KEY` | `minimax-cn` 路由;中国站订阅 Key |
59
83
  | Command Code | `COMMANDCODE_API_KEY` | 凭据由提供方插件管理,本页只读继承;也兜底读取 `~/.commandcode/auth.json`(`cmd login`) |
84
+ | SuperGrok | 不使用 API Key | `xai-oauth` 路由;只读 dsh-grok-kit / Grok CLI 共享的 OAuth 登录文件 `~/.grok/auth.json`(旧版 `~/.dsh/.xai-oauth-auth.json`),不保存、不刷新 token |
85
+ | 火山方舟 Ark | `VOLC_ACCESSKEY` + `VOLC_SECRETKEY` | 三个路由由**官方插件** [`@volcengine/ark-plan-api`](https://www.npmjs.com/package/@volcengine/ark-plan-api) 注册(`ark-coding-plan-cn` / `ark-agent-plan-cn` / `ark-coding-plan-byteplus`);额度查询要 **IAM Access Key 的 AK/SK 配对**,与这里的推理 API Key(`ARK_*_API_KEY`)**不是同一套**,三家共用一组 AK/SK |
86
+ | SiliconFlow | `SILICONFLOW_API_KEY` | 余额型:`GET /v1/user/info` 返回账户余额,**与推理是同一把 Key** |
87
+ | Codex | 不使用 API Key | `openai-codex` 路由由 DSH 内置或 `dsh-codex-connect` 等插件提供;只读 Codex CLI 的 ChatGPT 订阅登录文件(`$CODEX_HOME/auth.json` 或 `~/.codex/auth.json`,要求 `auth_mode` 为 `chatgpt`),不保存、不刷新 token。**API Key 模式(`auth_mode` 不是 `chatgpt`)取不到订阅额度**,此时按未配置处理 |
88
+ | OpenRouter | `OPENROUTER_API_KEY` | key 限额与用量用推理 key 即可;**账户余额需要 management / provisioning key**,普通 key 会被 403 拒——此时静默降级为只显示限额,不判失败 |
89
+ | Novita / Hyperbolic / DeepInfra / Chutes / Ollama Cloud / Vercel AI Gateway | `NOVITA_API_KEY` / `HYPERBOLIC_API_KEY` / `DEEPINFRA_API_KEY` / `CHUTES_API_KEY` / `OLLAMA_API_KEY` / `AI_GATEWAY_API_KEY` | 余额型聚合商,**与各自推理同一把 Key**。注意 Ollama Cloud 的鉴权是**裸 `Authorization`**(本插件已按其要求发送),且各家金额单位不同(见下表) |
60
90
 
61
- Z.ai(中国与国际)/ Kimi / OpenCode Go / MiniMax / Synthetic / NanoGPT 的继承模式按凭据服务 → 启动环境 → 旧手动配置兜底解析;自定义模式仅使用保存的手动 Key。Command Code 仅沿用提供方凭据来源链,不接受本插件的手动 Key;MiMo 仅使用 Cookie。界面区分凭据服务、启动环境和自定义来源。修改启动环境后是否需要重启取决于目标部署,不能把用户环境即时变化当作已经被运行进程读到。
91
+ Z.ai(中国与国际)/ Kimi / OpenCode Go / MiniMax / Synthetic / NanoGPT 的继承模式按凭据服务 → 启动环境 → 旧手动配置兜底解析;自定义模式仅使用保存的手动 Key。Command Code 仅沿用提供方凭据来源链,不接受本插件的手动 Key;MiMo 仅使用 Cookie;SuperGrok 仅使用共享 OAuth 登录文件(API Key 路线取不到订阅周池)。界面区分凭据服务、启动环境和自定义来源。修改启动环境后是否需要重启取决于目标部署,不能把用户环境即时变化当作已经被运行进程读到。
62
92
 
63
93
  ### MiMo 登录与 Cookie
64
94
 
@@ -92,22 +122,82 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
92
122
 
93
123
  **安全说明**:Key/Cookie 不再通过用量读取接口回传,界面不回填已保存的秘密;但本版仍兼容原本机 JSON 配置存储,**不是加密凭据库**。该配置位于 `~/.dsh/dsh-subusage.json`,新建目录/临时文件在 POSIX 上按 `0700`/`0600` 创建,替换前再次收紧文件权限;Windows 的实际访问权限仍取决于目录 ACL,`chmod` 不能替代 ACL 或加密库。请勿上传、分享或写入日志。隔离浏览器仅用于你主动发起的官方登录;它不是后台扫描或导入日常浏览器全部凭据的工具。加密凭据存储通道仍未实施。
94
124
 
125
+ ### 火山方舟 Ark 凭据
126
+
127
+ 火山方舟的套餐额度走**管控面 OpenAPI**(`open.volcengineapi.com`,HMAC-SHA256 签名),只能用 **IAM Access Key**(AK/SK 配对):推理用的方舟 API Key 交给上游会被拒绝,两者是两套凭据。三个 Ark 路由共用这一组 AK/SK。
128
+
129
+ 1. 在火山引擎控制台创建 Access Key,建议用子用户并只授予方舟只读权限(额度接口不接受主账号之外的其它鉴权方式)。
130
+ 2. 打开 **设置 → 订阅用量 → 提供商管理**,展开火山方舟任意一家的「连接与凭据」,填入 AccessKey ID 与 SecretAccessKey 并保存。
131
+ 3. 或在启动环境里设置 `VOLC_ACCESSKEY` / `VOLC_SECRETKEY`;也可以先把凭据写进 DSH 凭据服务,由「继承凭据 / 环境变量」模式读取。
132
+
133
+ AK 与 SK 必须来自同一个 IAM 用户:跨来源拼凑只会得到 401(`SignatureDoesNotMatch`),而报错本身看不出根因。两个字段各自「留空表示不修改该项」,清除要显式点「清除 AK/SK」——只有一半凭据时按未配置显示,不会拿半个签名去请求。
134
+
135
+ 额度按 provider 分派 Action:Coding Plan 走 `GetCodingPlanUsage`,Agent Plan 走 `GetAFPUsage`。账号没有对应套餐时接口返回 **HTTP 200 且窗口为空**,界面显示「未检测到该套餐订阅」,不会画成 0% 用量。401(签名/凭据)、403(权限或未订阅)、接口不存在三类失败分别给出不同的行动项。
136
+
137
+ > `GetCodingPlanUsage` 并未出现在官方 API 概览里(由官方 ark-cli 与多个第三方实现确证可用),存在变更风险;`GetAFPUsage` 有官方文档。
138
+
139
+ ### Codex(ChatGPT 订阅)凭据
140
+
141
+ Codex 的订阅额度走 ChatGPT 后端的私有接口(`GET https://chatgpt.com/backend-api/wham/usage`),凭据是 **Codex CLI 自己的 ChatGPT 登录**,不是 `OPENAI_API_KEY`:
142
+
143
+ - 登录文件:`$CODEX_HOME/auth.json`,默认 `~/.codex/auth.json`。
144
+ - 必须 `auth_mode` 为 `chatgpt`。若该文件是 API Key 模式,插件按「未配置」处理并在界面上说明——API Key 取不到订阅额度,不拿它去冒充订阅凭据。
145
+ - 插件**只读**这个文件:不保存、不刷新 token(token 由 Codex CLI 自己轮换,第三端刷新会互相顶掉)。请用 `codex login` 完成登录。
146
+
147
+ 窗口取 `rate_limit.primary_window`(5 小时)与 `secondary_window`(7 天;免费档是 30 天,按 `window_seconds` 判断窗口名)。`resets_at` 实测是 **Unix 秒数**(ISO 串也兼容),`plan_type` 作为套餐档位显示。`credits` 只在接口真正报告了可用余额时显示,`has_credits=false`、`unlimited` 或余额为 0 都不显示,不会把「没有额度」画成 0。接口返回 HTTP 200 但没有任何窗口时显示「未检测到订阅额度窗口」,不当作 0% 用量。
148
+
149
+ > 这是未公开的私有接口(CodexBar / cc-switch / QuotaRadar 三家实现互相印证),官方没有文档承诺,存在变更风险。
150
+
151
+ ### 额度查询 API(给其他插件与 Agent)
152
+
153
+ **Agent 工具**:`subusage_quota`。模型可以直接调用它查询各厂商的剩余额度与余额,参数 `providers`(可选,限定厂商 id)与 `refresh`(可选,绕过最多 60 秒的缓存)。工具**只读**:不写设置、不碰凭据。
154
+
155
+ **Host 侧服务**:本插件的 Host 服务 key 是 `subUsage`(cordis `Service`),其他插件可以直接消费:
156
+
157
+ ```js
158
+ const service = ctx.get("subUsage");
159
+ const view = await service.quota({ providerIds: ["deepseek"], force: false });
160
+ // view = { updatedAt, providers: [{ providerId, label, state, windows, extras, coverage, freshness, lastSuccessAt, error? }] }
161
+ ```
162
+
163
+ **只读且刻意精简**:`quota()` 不返回设置,也不返回 `keySource` / `apiDetected` / 继承变量名等内部状态——第三方消费者只需要额度,不需要知道本机配了哪些凭据,更不该碰到任何与 Key 相关的字段。`providerIds` 里的未知 id 会被忽略;单家读取失败只影响它自己的条目,其余照常返回。
164
+
165
+ 数据与设置页共用同一份缓存(TTL 60 秒),所以频繁调用不会反复打各家厂商接口。
166
+
95
167
  ## 支持的数据
96
168
 
97
169
  | 模型商 | 窗口 | 明细 |
98
170
  |---|---|---|
171
+ | DeepSeek | 无窗口(余额型) | 账户余额(`balance_infos` 的币种与总额);余额不可用时提示充值,不显示成 0 元 |
99
172
  | Z.ai 中国 | 5 小时、每周(按响应) | Token / Credits 配额、套餐档 |
100
173
  | Z.ai 国际 | 5 小时、每周(按响应) | Token / Credits 配额,区域凭据独立 |
101
174
  | Synthetic | 滚动订阅池 | 已用/总请求次数、重置时间 |
102
175
  | NanoGPT | 每日、每周、试用周期(按响应) | 输入 Token 数、重置时间;不包含图像额度 |
103
- | Kimi | 5 小时、7 天 | 已用百分比、套餐档 |
176
+ | Kimi | 5 小时、7 天、月度(按账户下发) | 已用百分比、套餐档 |
104
177
  | MiMo | 本周期额度池 | 用量明细、重置时间、套餐与余额 |
105
178
  | OpenCode Go | 滚动、每周、每月 | 已用百分比、重置时间 |
106
179
  | MiniMax 国际 / 中国 | 短周期(通常 5 小时)、每周 | 通用/编程池已用百分比、可信计数明细、重置时间 |
107
180
  | Command Code | 5 小时、每周、月额度池 | 已用/上限(月总额为套餐快照)、重置/账期、套餐(planId)、月剩余、已购 + 赠送余额 |
181
+ | SuperGrok | 周期池(通常每周,旧账户为月账期) | 统一用量池已用百分比(剩余 = 100 − 已用)、重置时间、套餐名(subscription_tier_display)、已购加量余额(美元) |
182
+ | Ark Coding Plan(中国 / BytePlus) | 5 小时、周、月 | 已用百分比、重置时间(接口只给百分比,不返回绝对量) |
183
+ | Ark Agent Plan(中国) | 5 小时、日、周、月 | AFP 已用/配额绝对值与百分比、重置时间、套餐档位(Small / Medium / Large / Max) |
184
+ | SiliconFlow | 无窗口(余额型) | 账户余额(元)。余额没有上限也就没有百分比,药丸直接显示余额,弹层与设置页列出明细 |
185
+ | Codex | 5 小时、7 天(免费档的次窗口是 30 天) | 已用百分比、重置时间;`credits` 只在真正报告了可用余额时显示(`has_credits=false`、`unlimited`、余额为 0 都不显示,不画成 0) |
186
+ | OpenRouter | 按 key 限额周期(月/周/日);免费档另有每日免费请求窗口 | 已用比例与限额明细;有 management key 时显示账户余额(`total_credits − total_usage`)。接口不给具体重置时刻,故不显示重置时间 |
187
+ | Novita / Hyperbolic / Vercel AI Gateway | 无窗口(余额型) | 账户余额(美元)。单位换算按各家接口:Novita 是 **1/10000 USD**、Hyperbolic 是**美分**、Vercel 是**十进制字符串** |
188
+ | DeepInfra | 无窗口(余额型) | 可用余额=`−stripe_balance`(接口用负数表示预付资金);欠款时单独提示,不显示成负余额 |
189
+ | Chutes | 通用限额窗口 | `{quota, used}` 绝对量;接口不给重置时刻,故不显示重置时间 |
190
+ | Ollama Cloud | session / 周 / 月 | `limits.*.usage` 是 **0–1 小数**;缺层跳过该窗口而不判失败 |
191
+ | ZenMux | 5 小时滚动窗口 | `usage_percentage` 是 **0–1 小数**,另附 flows 明细与 `resets_at`;有 Management Key 时显示 PAYG 余额。两个端点各拿各的——余额被拒不影响配额 |
192
+ | NanoGPT | 日 / 周 / 试用周期(按响应) | 输入 Token 数、重置时间、账户状态;另有账户余额(美元) |
193
+ | LiteLLM | 预算周期(`budget_reset_at` 存在时才显示重置) | 已用 / 预算与百分比;**没有预算上限时不编造百分比**,只如实显示已用金额 |
108
194
 
109
195
  非公开控制台接口可能变更;缺失或非法百分比不会当作零用量。部分数据、未知额度和暂时失败有明确状态,不能据此保证推理接口一定可用。
110
196
 
197
+ 评估过但未接入的厂商(Requesty / Portkey / Groq / Together / Cerebras 等)及理由见 [提供商覆盖与取舍](docs/provider-coverage.md),其中包括已由用户决策跳过的 Anthropic。
198
+
199
+ Kimi 的窗口集合随套餐体系变化:老套餐(节奏命名,如 Allegro)返回 5 小时与 7 天,新套餐(Go / Plus 命名)返回 5 小时与月度总额,因此同一插件在不同账号上显示的窗口数可以不同;月度池里的 Code 份额不是独立预算,不单独成窗。插件按接口实际下发的窗口解析,不要求固定集合。
200
+
111
201
 
112
202
  ## 常见问题
113
203
 
@@ -116,6 +206,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
116
206
  | 设置页没有提供商标签 | 展开提供商管理,确认开关和凭据;无凭据的条目默认隐藏,可临时关闭自动隐藏查看指引 |
117
207
  | 已配置凭据却没有药丸 | 确认当前模型的提供商 ID 与支持表一致、检测已开启;自定义 ID 可在设置页查看,但不会自动映射 |
118
208
  | 显示认证失效 | 检查是否用了对应产品/区域的订阅 Key;MiMo 重新登录或导入 Cookie;旧额度不会当作有效数据保留 |
209
+ | Kimi 缺少 7 天窗口,或提示结构错误 | 窗口集合按套餐体系下发:老套餐(节奏命名,如 Allegro)为 5 小时 + 7 天,新套餐(Go / Plus 命名)为 5 小时 + 月度总额。0.8.3 起按实际下发的窗口解析,不再要求 7 天窗口;升级后仍报「Invalid Kimi usage response」即为未识别的字段结构,插件不会猜测额度,可回报该响应 |
119
210
  | 显示缓存、部分数据或额度未知 | 查看更新时间和错误说明;缓存来自之前的成功读取,部分数据不保证有可用额度,余额也不等于订阅余量 |
120
211
  | 刷新后数字暂未变化 | 成功结果按提供商缓存 60 秒;手动刷新可跳过普通 TTL,但仍遵守限流退避和 Retry-After |
121
212
  | MiMo 自动登录无法启动 | Host 所在机器需安装 Google Chrome 并有桌面环境;远程或无桌面部署使用手动导入 |
@@ -125,6 +216,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
125
216
  | 保存提示配置已改变 | 另一窗口或实例更新了设置;重新读取配置后再编辑,避免旧表单覆盖新值 |
126
217
  | 升级后仍是旧界面或 RPC 不匹配 | 确认运行中的 Host 和 Client 都已重载;仅刷新页面不能保证 Host 升级 |
127
218
  | Command Code 额度与实际账户不同 | 可在药丸弹层切换提供方插件的服务账户(activeAccount),切换后药丸与弹层用量同步显示所选账户的额度;「自动轮换」时仍显示默认账户(顶层 Key)的用量,不跟随轮换中的实际服务账户,提供方自定义 apiBase 也不跟随 |
219
+ | SuperGrok 提示认证失效 | 登录文件里的 OAuth token 会过期;在 设置 → Grok Kit 重新登录、运行 `grok login`,或让 Grok 侧使用一次以刷新共享登录文件后重试。API Key(`XAI_API_KEY`)取不到订阅周池 |
128
220
 
129
221
  ## 提供商 ID 与默认开关
130
222
 
@@ -135,6 +227,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
135
227
  | MiMo | `xiaomi-token-plan-cn` | 开启 |
136
228
  | OpenCode Go | `opencode-go` | 开启 |
137
229
  | Command Code | `commandcode` | 开启 |
230
+ | SuperGrok | `xai-oauth` | 开启 |
138
231
  | MiniMax 国际 | `minimax` | 开启 |
139
232
  | MiniMax 中国 | `minimax-cn` | 开启 |
140
233
  | Z.ai 国际 | `zai-coding` | **关闭** |
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,38 @@ 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
+ "ark-coding-plan-cn": { label: "Ark Coding Plan (CN)", short: "Ark Coding", envName: "ARK_CODING_PLAN_CN_API_KEY", volc: true, defaultEnabled: false },
256
+ "ark-agent-plan-cn": { label: "Ark Agent Plan (CN)", short: "Ark Agent", envName: "ARK_AGENT_PLAN_CN_API_KEY", volc: true, defaultEnabled: false },
257
+ "ark-coding-plan-byteplus": { label: "Ark Coding Plan (BytePlus)", short: "Ark BytePlus", envName: "ARK_CODING_PLAN_BYTEPLUS_API_KEY", volc: true, defaultEnabled: false },
258
+ // 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
259
+ "siliconflow": { label: "SiliconFlow", short: "SiliconFlow", envName: "SILICONFLOW_API_KEY", defaultEnabled: false },
260
+ // 余额端点只对 management / provisioning key 开放;普通 key 只显示限额与用量。
261
+ "openrouter": { label: "OpenRouter", short: "OpenRouter", envName: "OPENROUTER_API_KEY", defaultEnabled: false },
262
+ // 余额型聚合商:单体 GET + 一个余额数字,药丸用余额兜底文案显示。
263
+ "novita": { label: "Novita AI", short: "Novita", envName: "NOVITA_API_KEY", defaultEnabled: false },
264
+ "hyperbolic": { label: "Hyperbolic", short: "Hyperbolic", envName: "HYPERBOLIC_API_KEY", defaultEnabled: false },
265
+ "deepinfra": { label: "DeepInfra", short: "DeepInfra", envName: "DEEPINFRA_API_KEY", defaultEnabled: false },
266
+ "chutes": { label: "Chutes", short: "Chutes", envName: "CHUTES_API_KEY", defaultEnabled: false },
267
+ "ollama-cloud": { label: "Ollama Cloud", short: "Ollama", envName: "OLLAMA_API_KEY", defaultEnabled: false },
268
+ "vercel-ai-gateway": { label: "Vercel AI Gateway", short: "Vercel AI", envName: "AI_GATEWAY_API_KEY", defaultEnabled: false },
269
+ // 额度端点只认 Management API Key,所以独立变量名。
270
+ "zenmux": { label: "ZenMux", short: "ZenMux", envName: "ZENMUX_MANAGEMENT_API_KEY", defaultEnabled: false },
271
+ // LiteLLM 是自建网关:除虚拟 Key 外还要填自己的 proxy 地址。
272
+ "litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
236
273
  };
237
- const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt"];
274
+ // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
275
+ 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"];
238
276
  function providerEnabled(settings, id) { return settings?.visibility?.providers?.[id] ?? (PROVIDER_META[id]?.defaultEnabled !== false); }
239
277
  function providerVisible(settings, id, entry) {
240
278
  if (!providerEnabled(settings, id)) return false;
@@ -302,7 +340,12 @@ window.__ModuleLoader__.load({
302
340
  }
303
341
  /** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
304
342
  function extraLabel(x, t) {
305
- return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : t("balanceLabel");
343
+ return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : x.kind === "spend" ? t("spendLabel") : t("balanceLabel");
344
+ }
345
+ /** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
346
+ function balanceText(entry, t) {
347
+ const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") : undefined;
348
+ return row ? `${t("balanceLabel")} ${row.value}` : null;
306
349
  }
307
350
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
308
351
  function durationText(ms, t) {
@@ -508,6 +551,39 @@ window.__ModuleLoader__.load({
508
551
  if (entry.coverage === "partial") return t("statusPartial");
509
552
  return t("statusOk");
510
553
  }
554
+ /**
555
+ * 需要用户行动时的内联入口类型。设置页的「状态」在上(用量面板)、「操作」在下(提供商管理),
556
+ * 出问题时用户得先去下面找到对应厂商才能修——这里让问题旁边直接出现修复动作。
557
+ * 正常状态返回 null:不打扰,也不重复管理区那份编辑器。
558
+ */
559
+ function usageActionKind(entry, providerId, t) {
560
+ if (!entry || entry.state === "loading" || entry.state === "disabled") return null;
561
+ const authFailed = /auth|credential/i.test(entry.errorCode || "");
562
+ if (providerId === "xiaomi-token-plan-cn") {
563
+ const expiry = cookieExpiry(entry, t);
564
+ if (expiry && expiry.expired) return "mimo-relogin";
565
+ // rank ≥ 2 才是「临近到期」(与药丸同一档位判定),否则正常倒计时不必打扰。
566
+ if (entry.state === "no-cookie" || authFailed || (expiry && expiry.rank >= 2)) return "mimo-login";
567
+ return null;
568
+ }
569
+ if (entry.state === "no-key" || entry.state === "no-cookie" || authFailed) return "credentials";
570
+ return null;
571
+ }
572
+ /**
573
+ * 待处理程度的排序权重:读取失败与 Cookie 过期最急,额度用尽与即将到期次之,数据不完整再次。
574
+ * **「未配置凭据」刻意不算**——那是用户自己的取舍(默认隐藏就是为收起它们),
575
+ * 把它顶到最前面会让每个还没配的厂商都来抢位置。
576
+ */
577
+ function usageAttentionRank(entry, t) {
578
+ if (!entry) return 0;
579
+ if (entry.state === "error") return 3;
580
+ const expiry = cookieExpiry(entry, t);
581
+ if (expiry && expiry.expired) return 3;
582
+ if ((entry.windows || []).some(w => w.status === "rate-limited")) return 2;
583
+ if (expiry && expiry.rank >= 2) return 2;
584
+ if (entry.freshness === "stale" || entry.coverage === "partial") return 1;
585
+ return 0;
586
+ }
511
587
  function providerDescription(entry, t) {
512
588
  const windows = canShowUsage(entry) ? entry.windows || [] : [];
513
589
  const worst = worstWindow(windows);
@@ -749,12 +825,21 @@ window.__ModuleLoader__.load({
749
825
  h("span", { style: { flex: 1 } }, labelText),
750
826
  ccState.activeId === id ? h("span", { style: { fontSize: 11, opacity: 0.75 } }, t("ccAccountActive")) : null
751
827
  );
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")),
828
+ // 账户多时这一坨会把上方的额度顶出视野:折进 details,summary 只留「当前账户 + 数量」,
829
+ // 展开后列表自身限高滚动,弹层高度不随账户数增长。
830
+ const activeLabel = ccState.status !== "ready" ? null
831
+ : ccState.activeId === "" ? t("ccAccountAuto")
832
+ : ccState.activeId === "default" ? t("ccAccountDefault")
833
+ : (ccState.accounts.find(account => account.id === ccState.activeId)?.label ?? ccState.activeId);
834
+ const optionCount = ccState.status === "ready" ? ccState.accounts.length + 1 : 0;
835
+ return h("details", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
836
+ h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } },
837
+ `${t("ccAccountsTitle")}:${activeLabel ?? (ccState.status === "loading" || ccState.status === "idle" ? t("loading") : t("ccAccountsUnavailable"))}${optionCount > 1 ? `(${optionCount})` : ""}`),
754
838
  ccState.status === "ready"
755
839
  ? h("div", null,
756
- optionButton("", t("ccAccountAuto")),
757
- ccState.accounts.map((account) => optionButton(account.id, account.id === "default" ? t("ccAccountDefault") : account.label)),
840
+ h("div", { style: { maxHeight: 240, overflowY: "auto" } },
841
+ optionButton("", t("ccAccountAuto")),
842
+ ccState.accounts.map((account) => optionButton(account.id, account.id === "default" ? t("ccAccountDefault") : account.label))),
758
843
  ccState.error ? h("p", { style: { color: "#f87171", margin: "6px 0 0", whiteSpace: "pre-wrap" } }, `${t("ccSwitchFailed")}: ${ccState.error}`) : null,
759
844
  h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.65 } }, t("ccAccountsHint")))
760
845
  : h("p", { style: { margin: "4px 0 0", opacity: 0.7 } }, ccState.status === "loading" || ccState.status === "idle" ? t("loading") : t("ccAccountsUnavailable"))
@@ -886,11 +971,11 @@ window.__ModuleLoader__.load({
886
971
  const worst = windows.length > 0 ? worstWindow(windows) : null;
887
972
  const worstTier = worst ? usageTier(worst) : null;
888
973
  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"));
974
+ const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
890
975
  const stateText = !ok ? (snapshot && snapshot.entry ? snapshot.entry.state : failState) : void 0;
891
976
  const stateLabel = rawEntry ? statusText(rawEntry, t) : failState === "no-key" ? t("statusNoKey") : failState === "no-cookie" ? t("statusNoCookie") : failed ? t("statusError") : t("loading");
892
977
  // 配置缺失类失败给可行动指引,不倒裸报错。
893
- const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : t("pillNeedKey"))
978
+ const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
894
979
  : stateText === "no-cookie" ? t("pillNeedCookie")
895
980
  : null;
896
981
  const isConfig = stateText === "no-key" || stateText === "no-cookie";
@@ -907,7 +992,7 @@ window.__ModuleLoader__.load({
907
992
  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
993
  const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
909
994
  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;
995
+ 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
996
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
912
997
  h("button", {
913
998
  type: "button",
@@ -974,13 +1059,30 @@ window.__ModuleLoader__.load({
974
1059
  return PROVIDER_ORDER[0];
975
1060
  }
976
1061
  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) } };
1062
+ 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
1063
  }
979
1064
  function settingsPatch(id, draft, revision) {
980
1065
  if (typeof revision !== "string" || !revision) throw new Error("请先加载最新设置,再编辑保存 / Load settings before saving");
981
1066
  const patch = { providerId: id, expectedRevision: revision };
982
1067
  // 凭据由提供方插件管理的模型商没有任何本地凭据补丁可保存。
983
1068
  if (PROVIDER_META[id].managedByPlugin) return patch;
1069
+ if (PROVIDER_META[id].volc) {
1070
+ // AK/SK 是秘密且不回填:只有真正填了值的那一项才发 replace,避免「只改 AK 把 SK 抹掉」;
1071
+ // 清除必须显式点按钮,空输入不是清空指令。
1072
+ patch.keyMode = draft.keyMode;
1073
+ if (draft.volc.clear) patch.volc = { accessKeyId: { action: "clear" }, secretAccessKey: { action: "clear" } };
1074
+ else {
1075
+ const volc = {};
1076
+ const accessKey = draft.volc.accessKey.trim(), secret = draft.volc.secret.trim();
1077
+ for (const [field, value] of [["accessKeyId", accessKey], ["secretAccessKey", secret]]) {
1078
+ if (!value) continue;
1079
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error("Access Key 不能包含控制字符");
1080
+ volc[field] = { action: "replace", value };
1081
+ }
1082
+ if (Object.keys(volc).length) patch.volc = volc;
1083
+ }
1084
+ return patch;
1085
+ }
984
1086
  if (id === "xiaomi-token-plan-cn") {
985
1087
  patch.cookieUpdate = { action: draft.cookieAction };
986
1088
  if (draft.cookieAction === "replace") patch.cookieUpdate.value = validateCookieText(draft.cookie);
@@ -992,6 +1094,8 @@ window.__ModuleLoader__.load({
992
1094
  patch.keyUpdate.value = draft.key.trim();
993
1095
  }
994
1096
  if (id === "zai-coding-cn") patch.zai = { ...draft.zai };
1097
+ // LiteLLM 的代理地址不是秘密,原样提交;空串表示清空(请求时会按「未配置」提示)。
1098
+ if (id === "litellm") patch.litellm = { baseUrl: draft.litellm.baseUrl.trim() };
995
1099
  }
996
1100
  return patch;
997
1101
  }
@@ -1130,8 +1234,15 @@ window.__ModuleLoader__.load({
1130
1234
  const primary = { ...btnStyle, background: "rgba(59,130,246,.12)", borderColor: "rgba(59,130,246,.65)" };
1131
1235
  const entries = result.entries || [];
1132
1236
  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)));
1237
+ // 待处理的排前面;组内保持登记顺序,所以顺序不会自己抖动。
1238
+ const visibleIds = PROVIDER_ORDER
1239
+ .map((id, index) => ({ id, index, rank: usageAttentionRank(entries.find(e => e.providerId === id), t) }))
1240
+ .filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id)))
1241
+ .sort((a, b) => b.rank - a.rank || a.index - b.index)
1242
+ .map(item => item.id);
1134
1243
  const success = entries.filter((e) => visibleIds.includes(e.providerId) && e.state === "ok" && e.freshness !== "stale").length;
1244
+ // 顶部计数同时是「出问题的那几家在哪」的入口:点一下就切到第一家没读成功的。
1245
+ const troubledIds = visibleIds.filter(id => { const e = entries.find(item => item.providerId === id); return !(e && e.state === "ok" && e.freshness !== "stale"); });
1135
1246
  const hideWithoutApi = result.settings?.visibility?.hideWithoutApi !== false;
1136
1247
  react.useEffect(() => {
1137
1248
  if (visibleIds.length && !visibleIds.includes(usageTab)) setUsageTab(visibleIds[0]);
@@ -1139,6 +1250,15 @@ window.__ModuleLoader__.load({
1139
1250
  const usageId = visibleIds.includes(usageTab) ? usageTab : visibleIds[0];
1140
1251
  const usageEntry = entries.find(e => e.providerId === usageId);
1141
1252
  const usageMeta = PROVIDER_META[usageId];
1253
+ // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1254
+ const usageAction = usageActionKind(usageEntry, usageId, t);
1255
+ // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1256
+ const revealEditor = (target) => {
1257
+ openEditor(target);
1258
+ const box = root.current?.querySelector?.("#subusage-provider-management");
1259
+ if (box) box.open = true;
1260
+ setTimeout(() => { const el = root.current?.querySelector?.(`#subusage-credentials-${target}`); if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" }); }, 0);
1261
+ };
1142
1262
  react.useEffect(() => {
1143
1263
  if (!usageId) return;
1144
1264
  const deactivate = usageStore.activate(usageId);
@@ -1147,7 +1267,7 @@ window.__ModuleLoader__.load({
1147
1267
  }, [usageStore, usageId]);
1148
1268
  const meta = PROVIDER_META[tab];
1149
1269
  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)") };
1270
+ 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(tab === "xai-oauth" ? "来自 ~/.grok/auth.json(Grok / Grok Kit 登录)" : "来自 ~/.commandcode/auth.json(cmd login)", tab === "xai-oauth" ? "From ~/.grok/auth.json (Grok / Grok Kit sign-in)" : "From ~/.commandcode/auth.json (cmd login)") };
1151
1271
  const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1152
1272
  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
1273
  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 +1281,18 @@ window.__ModuleLoader__.load({
1161
1281
  tab === "synthetic" ? h("p", null, text("使用 Synthetic API Key,仅显示模型订阅请求额度。", "Use a Synthetic API key. Shows the model subscription request quota.")) : null,
1162
1282
  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
1283
  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,
1284
+ 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,
1285
+ 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,
1286
+ 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; all three Ark plans share this pair.")),
1287
+ h("div", { role: "group", "aria-label": text("Key 来源模式", "Key source mode"), style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1288
+ ["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")}`))),
1289
+ h("p", { style: { fontSize: 12 } }, text("继承变量:", "Inherited variables: "), h("code", null, "VOLC_ACCESSKEY"), " / ", h("code", null, "VOLC_SECRETKEY")),
1290
+ 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")),
1291
+ (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,
1292
+ draft.volc.clear ? h("span", { role: "status" }, text("将删除 AK/SK(尚未保存)", "AK/SK will be deleted (not saved yet)")) : null,
1293
+ 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 })),
1294
+ 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 })),
1295
+ 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
1296
  h("button", { type: "button", style: primary, disabled: loginActive, onClick: startLogin }, text("登录并自动导入", "Sign in & import automatically")),
1166
1297
  loginActive ? h("div", { role: "status", "aria-live": "polite", style: { marginTop: 8 } },
1167
1298
  h("progress", { "aria-label": text("登录进度", "Login progress"), style: { width: 100, marginRight: 8 } }),
@@ -1183,12 +1314,15 @@ window.__ModuleLoader__.load({
1183
1314
  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
1315
  credentialButtons("key", result.settings && result.settings.hasKeys && result.settings.hasKeys[tab]),
1185
1316
  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),
1317
+ tab === "litellm" ? h(react.Fragment, null,
1318
+ 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 })),
1319
+ 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
1320
  tab === "zai-coding-cn" ? h(react.Fragment, null,
1187
1321
  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
1322
  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
1323
  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
1324
  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,
1325
+ 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
1326
  entry && entry.lastAttemptAt ? h("p", null, `${t("lastUpdated")} ${new Date(entry.lastAttemptAt).toLocaleString(getLocale())}`) : null)
1193
1327
  );
1194
1328
  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 +1339,7 @@ window.__ModuleLoader__.load({
1205
1339
  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
1340
  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
1341
  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")),
1342
+ 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
1343
  tab === id ? credentialEditor : null));
1210
1344
  }))
1211
1345
  )
@@ -1213,7 +1347,14 @@ window.__ModuleLoader__.load({
1213
1347
  return h("div", { ref: root, style: { padding: 16, maxWidth: 720, minWidth: 0, font: "13px/1.6 sans-serif", containerType: "inline-size", containerName: "subusage" } },
1214
1348
  h("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" } },
1215
1349
  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`)),
1350
+ h("button", {
1351
+ type: "button",
1352
+ disabled: !troubledIds.length,
1353
+ onClick: () => { if (troubledIds.length) chooseUsage(troubledIds[0]); },
1354
+ 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"),
1355
+ "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"}`),
1356
+ style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
1357
+ }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
1217
1358
  h("button", { type: "button", disabled: busy || !usageId, style: primary, onClick: () => refresh(false) }, text("刷新当前", "Refresh current")),
1218
1359
  h("button", { type: "button", disabled: busy, style: btnStyle, onClick: () => refresh(true) }, text("刷新全部", "Refresh all"))
1219
1360
  ),
@@ -1239,6 +1380,10 @@ window.__ModuleLoader__.load({
1239
1380
  h("p", { role: "status", style: { margin: "4px 0", color: usageEntry && usageEntry.state !== "ok" ? "#dc2626" : "inherit" } }, statusText(usageEntry, t)),
1240
1381
  cacheAge(usageEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(usageEntry, t)) : null,
1241
1382
  usageEntry && usageEntry.error ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1383
+ 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)" } },
1384
+ 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.")),
1385
+ 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,
1386
+ h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1242
1387
  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
1388
  canShowUsage(usageEntry) && usageEntry.extras ? h("p", { style: { opacity: .75 } }, usageEntry.extras.map((x) => `${extraLabel(x, t)}: ${x.value}`).join(" · ")) : null,
1244
1389
  usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null