dsh-subusage 0.11.2 → 0.13.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,116 +1,115 @@
1
1
  <div align="center">
2
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/title.png" width="100%" alt="dsh-subusage —— 模型选择器旁的余量药丸,右侧是同一界面的浅色与深色用量弹层(真实组件离线渲染示例)">
3
2
 
4
- # 订阅用量显示
3
+ # 订阅用量显示
5
4
 
6
- **选中的模型还剩多少额度,输入框旁边一眼可见。**
5
+ **额度与余额,不必离开对话。**
7
6
 
8
- dsh-subusage 把已支持的 27 条模型提供商路由额度装进 DeepSeek Harness 的输入框旁边——不切控制台、不猜百分比、不等报错。
7
+ 在 DeepSeek Harness 的模型选择器旁看余量;点开药丸,查看额度窗口、重置时间与余额。
9
8
 
10
- [![npm version](https://img.shields.io/badge/npm-0.11.2-blue)](https://www.npmjs.com/package/dsh-subusage)
11
- [![license](https://img.shields.io/badge/license-MIT-green)](#license--security--许可与安全)
12
- [![DSH](https://img.shields.io/badge/DSH-0.2.x-informational)](#compatibility--兼容性)
9
+ [![npm](https://img.shields.io/npm/v/dsh-subusage?color=3b82f6&label=npm)](https://www.npmjs.com/package/dsh-subusage)
10
+ [![license](https://img.shields.io/badge/license-MIT-64748b)](#license--security--许可与安全)
11
+ [![DSH Core](https://img.shields.io/badge/DSH_Core-0.2.x-64748b)](#compatibility--兼容性)
12
+
13
+ [快速开始](#快速开始) · [功能预览](#功能预览) · [支持清单](#支持的数据--supported-data) · [凭据指引](docs/credentials.md)
14
+
15
+ <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/title.png" width="1200" alt="dsh-subusage:保留原标题图的深蓝构图,右侧展示真实 Codex 与 OpenCode Go 深浅主题用量弹层;虚构数据离线预览">
16
+
17
+ <sub>真实组件 · 虚构数据 · 真实组件离线预览,不是运行中的 DSH 截图</sub>
13
18
 
14
- **安装只需一条命令** → [快速开始](#快速开始)
15
19
  </div>
16
20
 
17
- ---
21
+ > **0.13.0**:余额显示更清晰,设置按需刷新,兼容 Codex Subscription 独立 OAuth。发布与本机升级分开;本页图片是虚构数据离线预览,不是实机验收证明。[查看更新记录](docs/changelog.md)。
18
22
 
19
- ## 为什么需要它
23
+ ## 快速开始
20
24
 
21
- | 没有它 | 有了它 |
22
- |---|---|
23
- | 额度散落在五六家控制台,查一次余额要开五个标签页 | 药丸常驻输入框旁:选中哪家模型就看哪家,多窗口自动取最差一窗 |
24
- | Agent 跑到一半撞上 429,才知道额度用尽 | 弹层先给结论再给依据:「即将用尽 · 最紧张:每周 · 剩 5% · 3 天后重置」 |
25
- | Cookie、OAuth 悄悄过期,报错才发现 | MiMo Cookie 提前 2 小时变黄、30 分钟变橙、过期变红;认证失效直说原因与修法 |
26
- | 每家插件各管各的凭据,格式五花八门 | 一个设置页集中管理 27 条路由的开关与凭据,缺什么补什么 |
25
+ ```console
26
+ dsh plugin --profile web add dsh-subusage
27
+ ```
27
28
 
28
- ## 状态,一眼分清
29
+ `web` 是示例 Profile,请替换为实际目标。**Desktop 使用目标桌面壳提供的 DSH Desktop CLI**,不要从独立公共 CLI 管理保留的 `desktop` Profile。
29
30
 
30
- 药丸不写字也能传达状态:每个额度窗口一根彩条(绿 / 黄 / 橙 / 红,满格 = 100% 剩余),需要留意时才追加一个词。下面是已发布基础状态的离线预览(虚构用量数据);0.11.2 新增的续用/预算/能力范围状态见下节:
31
+ 1. **启用插件**:按部署方式重载,让 Host 与 Client 一起生效;只刷新页面不保证 Host 已升级。
32
+ 2. **配置需要的提供商**:打开 **设置 → 订阅用量 → 提供商管理**。填 API Key,或使用对应提供方插件的登录态;MiMo 可“登录并自动导入”。[不知道该填什么?](docs/credentials.md)
33
+ 3. **回到对话**:选择该提供商的模型,输入区出现用量药丸。点开看明细;读取失败会说明原因,不把未知当充足。
31
34
 
32
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-light.png" width="100%" alt="药丸 15 种状态(浅色,离线预览)">
35
+ **已经登录 DSH 的 DeepSeek 账号?** 默认开启的「DeepSeek 账号」直接读取宿主余额,不需要复制 token。独立 API Key 使用另一个「DeepSeek API」入口,两份余额不相加、不互相替代。[两种入口的区别](docs/providers.md#deepseek-api-与-deepseek-账号)
33
36
 
34
- <details>
35
- <summary>深色主题</summary>
37
+ ## 功能预览
36
38
 
37
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-dark.png" width="100%" alt="药丸 15 种状态(深色,离线预览)">
39
+ <picture>
40
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/readme/features-dark.png">
41
+ <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/readme/features-light.png" width="1200" alt="三块真实组件:Codex 套餐用量、独立 DeepSeek 账号余额、Command Code 多账户轮换池;虚构数据离线预览">
42
+ </picture>
38
43
 
39
- </details>
44
+ ### 看额度:最紧张的窗口,先提醒你
40
45
 
41
- ## 三块能力,一个设置页
46
+ 一个窗口一根余量条。正常时保持简洁;周窗偏少、额度耗尽、读取失败时,药丸明确提示。点开后再看百分比、适用范围与重置倒计时,不需要先切到控制台。
42
47
 
43
- | 能力 | 解决什么问题 | 入口 |
44
- |---|---|---|
45
- | 余量药丸 | 不用切到各家控制台,选中模型就知道这家还剩多少 | 模型选择器左侧,槽位 `conversation.input.right` |
46
- | 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
47
- | 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
48
+ ### 看余额:金额直接显示,单位不混淆
48
49
 
49
- 药丸只画图形:每个额度窗口一根彩色余量条(绿 / 黄 / 橙 / 红,满 = 100% 剩余),正常时不写字;需要留意时追加一个词,如 `周 偏少`、`5时 已用尽`、`需配置`、`拉取失败`。成功结果按提供商缓存 **60 秒**,手动刷新可跳过普通 TTL(仍遵守上游限流退避与 `Retry-After`)。
50
+ 无套餐窗口且数据完整时,药丸直接显示金额。人民币、美元与 credits 各按自己的规则展示;**余额不等于订阅额度,也不能盖过已耗尽的套餐**。DeepSeek API 与本机账号有各自的开关、凭据来源与余额。
50
51
 
51
- 本插件**不注册模型路由**,药丸按 provider id 匹配;因此使用哪个插件提供该模型路由不影响匹配,id 对不上时才不显示。
52
+ ### 管账户:需要时展开,不让长列表占满界面
52
53
 
53
- ## 27 条提供商路由,默认开启 10 条
54
+ Command Code 可在弹层和设置页切换固定账户,或查看自动轮换池的健康度。逐账户详情默认折叠,受限周期明确标注。**自动轮换模式的用量仍是默认账户,不跟随实际服务中的账户。**
54
55
 
55
- DeepSeek、火山方舟(Agent / Coding Plan 个人版与企业版席位)、Z.ai(中国与国际)、Kimi、小米 MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅)、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM——订阅窗口、余额、周期额度池都支持。**新安装默认开启 10 条**,其余在设置页一键开启;完整清单、额度类型与鉴权方式见[下方技术文档](#支持的数据--supported-data)。
56
+ ## 支持什么
56
57
 
57
- ## 额度与续用:0.11.2 的关键修正
58
+ **28 条数据入口,新安装默认开启 11 条。** 按使用场景,主要分为:
58
59
 
59
- - **Codex Pro / ProLite / ProMax**:合法单周窗口正常展示,不再因没有 5 小时窗而黄警;多工作区查询携带账户身份,企业支出限制与 Credits 单独保留。
60
- - **ZenMux / Novita / LiteLLM**:补齐 ZenMux 周窗;Novita 接受官方金额字符串;LiteLLM 的 Key、用户、团队预算分别展示,任何一层都不会被更宽松的预算覆盖。
61
- - **包含额度耗尽不等于服务停摆**:Command Code 有已购余额时显示“按量续用”;NanoGPT 使用接口路由建议,未知余额/策略明确待确认。其他厂商未核验的续用条件不作保证。
62
- - **轮换与能力范围**:自动轮换池尚有健康账户,不因默认账户限额报全池耗尽;AFP 文本周限制不连坐日能力池,共享月额度仍影响各能力。部分能力耗尽只提示范围,不冒充整个套餐不可用。
63
- - **数据完整性**:合法单窗、无上限和纯余额成功不是“不完整”;真实端点读取失败仍可标部分覆盖。金额或窗口未知时不编造百分比。
60
+ | 你想看什么 | 代表提供商 |
61
+ |---|---|
62
+ | 订阅窗口与重置时间 | Kimi、MiMo、Codex、SuperGrok、MiniMax、Z.ai、OpenCode Go |
63
+ | 账号钱包与 API 余额 | DeepSeek API / 账号、SiliconFlow、Novita、Hyperbolic、DeepInfra |
64
+ | 套餐、余额与多层预算 | Command Code、NanoGPT、OpenRouter、ZenMux、LiteLLM |
65
+ | 账号级套餐与企业席位 | 火山方舟 Agent Plan / Coding Plan |
64
66
 
65
- 以上均已通过离线回归;不代表真实企业席位、新旧私有接口或付费资格已经在线验收。详细契约与仍待核验范围见[开发说明](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md#额度语义修正0112)。
67
+ [查看完整支持清单](#支持的数据--supported-data) · [查看哪些经过真实账号验证](#support-status--实测覆盖)
66
68
 
67
- [快速开始](#快速开始) · [支持清单](#支持的数据--supported-data) · [实测范围](#support-status--实测覆盖) · [凭据指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md) · [更新记录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/changelog.md)
69
+ 本插件**只显示用量,不注册模型路由**。Codex 默认关闭,避免与提供方自带药丸重复;需要统一展示时可手动开启。私有额度接口可能变化,数字仅供参考,不是计费依据。
68
70
 
69
- ## 快速开始
71
+ ## 也可以直接问 Agent
70
72
 
71
- ```console
72
- dsh plugin --profile <profile> add dsh-subusage
73
- ```
73
+ “看一下 Kimi 和 Codex 还剩多少额度。”
74
74
 
75
- 1. 安装并启用 bundle,按部署方式重载或重启(只刷新页面不保证 Host 升级)。
76
- 2. 打开 **设置 → 订阅用量 → 提供商管理**,开启需要的提供商并填凭据——每类凭据的官网入口与填入位置见[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md);MiMo 用「登录并自动导入」。
77
- 3. 回到会话,选中该提供商的模型:输入区左侧出现药丸,点开看窗口明细。
78
- 4. 排障:点「立即刷新」→ 看设置页顶部 `X/Y 家数据获取成功`(可点击,直接跳到第一家没读成功的)。
75
+ 模型可调用只读工具 `subusage_quota`,读取与界面同源的额度与余额;可按提供商筛选,Command Code 可分别返回各账户状态。不会通过该工具修改设置。[工具与 Host 服务契约](#额度查询-api)
79
76
 
80
- 可复现的最小示例(DeepSeek,余额型,默认开启,与推理同一把 Key):
77
+ ## 使用前,先知道这三点
81
78
 
82
- ```powershell
83
- # 任选一种凭据来源
84
- $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需按部署方式重启
85
- # 或在 设置 → 订阅用量 → DeepSeek →「连接与凭据」里手动填写
86
- ```
79
+ - **不猜数据**:查询失败、缓存与部分覆盖会明确标注;接口读取成功不保证仍可续用。
80
+ - **不把凭据放进截图或结果**:API Key、Cookie 与 OAuth token 不回填界面;但本插件手动配置仍可能明文落盘,**它不是加密凭据库**。
81
+ - **验证有范围**:历史真实账号记录覆盖 7 个入口,其余主要依靠桩测试;新图是虚构数据离线渲染,不能作为真实账号在线验收证明。[实测范围与风险](#support-status--实测覆盖)
87
82
 
88
- 重启后在会话中选中 DeepSeek 模型,药丸显示账户余额;也可以直接让 Agent 调用 `subusage_quota(providers: ["deepseek"])` 验证(无需界面)。
83
+ <details>
84
+ <summary><strong>版本与验证范围:本页截图展示了什么?</strong></summary>
85
+
86
+ 标题图和三功能特写由当前 `lib/client.js` 的真实组件生成,未制作平行 UI。画面使用虚构金额与账户;绿色余额图标、最多两位小数与独立 Codex Subscription OAuth 兼容纳入 **0.13.0**。OAuth 续约仍由凭据所属插件负责;未替代真实账号在线验收。
89
87
 
90
- ## 先说清楚:哪些是实测过的
88
+ 0.12.0 已发布双 DeepSeek 入口、分单位余额预警与 Command Code 详情折叠;该版本正常余额为灰色。历史截图与每轮验证范围见[更新记录](docs/changelog.md)和[开发说明](docs/development.md)。
91
89
 
92
- **7 家在真实账号上跑通**:火山方舟 Agent Plan、火山方舟 Coding Plan、Kimi Coding、小米 MiMo、Command Code、SuperGrok、Codex (ChatGPT)。**其余只经过桩测试**——响应形态取自公开文档、第三方实现或抓包,没有在真实账号上跑通,可能随接口变化而失效;失效时在设置页关掉那一家即可,其余不受影响。验证范围与主要风险见[实测覆盖](#support-status--实测覆盖)。额度数字仅供参考,不要当计费依据。
90
+ </details>
93
91
 
94
92
  ---
95
93
 
96
94
  # 技术文档
97
95
 
98
- > 安装、配置、排障的完整参考。快速上手看上面就够了。
96
+ 安装、配置与排障参考。日常使用先看上面的快速开始,需要定位具体接口或凭据来源时再展开下文。
99
97
 
100
98
  ## 支持的数据 / Supported data
101
99
 
102
- 共 **27 条 provider id**:新安装**默认开启 10 条、默认关闭 17 条**(数字与清单取自 [`lib/index.js`](lib/index.js) 顶部的 `PROVIDERS`,展示顺序与短名取自 [`lib/client.js`](lib/client.js) 顶部的 `PROVIDER_META` / `PROVIDER_ORDER`)。
100
+ 共 **28 条 provider id**:新安装**默认开启 11 条、默认关闭 17 条**(数字与清单取自 [`lib/index.js`](lib/index.js) 顶部的 `PROVIDERS`,展示顺序与短名取自 [`lib/client.js`](lib/client.js) 顶部的 `PROVIDER_META` / `PROVIDER_ORDER`)。
103
101
 
104
102
  | 提供商 | provider id | 额度类型 | 鉴权 | 默认 |
105
103
  |---|---|---|---|---|
106
- | DeepSeek | `deepseek` | 余额(账户) | Bearer,与推理同一把 Key | 开 |
104
+ | DeepSeek API | `deepseek` | 余额(账户) | Bearer,与推理同一把 Key | 开 |
105
+ | DeepSeek 账号 | `deepseek-account` | 余额(本机账号充值) | 复用 DSH `deepseekAccount` 登录态,无独立 Key | 开 |
107
106
  | Z.ai(中国) | `zai-coding-cn` | 订阅窗口 | Bearer(团队档另加组织 / 项目请求头) | 开 |
108
107
  | Kimi Coding | `kimi-coding` | 订阅窗口 | Bearer | 开 |
109
108
  | Xiaomi MiMo | `xiaomi-token-plan-cn` | 周期额度池 | 官方平台 Cookie(含登录流程) | 开 |
110
109
  | OpenCode Go | `opencode-go` | 订阅窗口 | Bearer | 开 |
111
110
  | Command Code | `commandcode` | 5 小时 / 周 / 月额度池 | 提供方插件的凭据链 | 开 |
112
111
  | SuperGrok | `xai-oauth` | 周期池 | 共享 Grok CLI 登录文件,过期经 grok-kit 续约 | 开 |
113
- | Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Codex CLI 的 OAuth 登录文件 | 关 |
112
+ | Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Subscription 独立 OAuth(0.13.0 起)或 Codex CLI 登录文件 | 关 |
114
113
  | MiniMax(中国) | `minimax-cn` | 短周期 / 周 | Bearer | 开 |
115
114
  | ARK Agent Plan (arkcli) | `arkcli-agent-plan` | 订阅窗口(AFP) | IAM AK/SK 签名 | 开 |
116
115
  | ARK Coding Plan (arkcli) | `arkcli-coding-plan` | 订阅窗口 | IAM AK/SK 签名 | 开 |
@@ -156,7 +155,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
156
155
 
157
156
  其余提供商**只经过桩测试**——响应形态取自公开文档、第三方实现或抓包,**没有在真实账号上跑通过**:
158
157
 
159
- > Z.ai(中国与国际)、OpenCode Go、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、DeepSeek、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM,以及火山方舟的**团队版席位**。
158
+ > Z.ai(中国与国际)、OpenCode Go、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、DeepSeek API、DeepSeek 账号、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM,以及火山方舟的**团队版席位**。
160
159
 
161
160
  它们可能可用,也可能因为接口变更、字段差异或权限要求而失败。上表也只是既有账号、当时版本的验证记录,不是持续可用性保证。
162
161
 
@@ -194,7 +193,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
194
193
 
195
194
  ```console
196
195
  # npm 包名(推荐:预构建,免构建授权)
197
- dsh plugin --profile <profile> add dsh-subusage
196
+ dsh plugin --profile web add dsh-subusage
198
197
 
199
198
  # GitHub 固定 Commit(DSH STORE / awesome-dsh-plugin 只接受这种来源)
200
199
  dsh plugin --profile <profile> add 'git+https://github.com/KouzakiUmi/dsh-subusage.git#<40 位完整 Commit>'
@@ -228,7 +227,7 @@ dsh plugin --profile <profile> remove dsh-subusage
228
227
  | --- | --- | --- |
229
228
  | Command Code | [`@mars-sea/dsh-commandcode-provider`](https://www.npmjs.com/package/@mars-sea/dsh-commandcode-provider) | 在该插件的设置页登录,或运行 `cmd login`(兜底读 `~/.commandcode/auth.json`) |
230
229
  | SuperGrok | [`dsh-grok-kit`](https://github.com/KouzakiUmi/dsh-grok-kit) | 运行 `grok login`,登录文件 `~/.grok/auth.json` |
231
- | Codex(ChatGPT 订阅) | [`dsh-codex-connect`](https://github.com/franksong2702/dsh-codex-connect)(npm 同名包) | 运行 `codex login`,登录文件 `$CODEX_HOME/auth.json` 或 `~/.codex/auth.json` |
230
+ | Codex(ChatGPT 订阅) | [`dsh-codex-connect`](https://github.com/franksong2702/dsh-codex-connect) 或 [`dsh-codex-subscription`](https://github.com/WSL043/dsh-codex-subscription)(0.13.0 起兼容) | Connect / CLI:`codex login`;Subscription:在其设置页独立 OAuth 登录,无需 CLI |
232
231
 
233
232
  > **Codex 默认关闭**:它的提供方插件本身就带一个用量药丸,两个并排只是重复信息。想要本插件这一份(比如想把 Codex 和别家放在一起看)可以在**提供商管理**里手动开启。
234
233
  >
@@ -238,7 +237,7 @@ dsh plugin --profile <profile> remove dsh-subusage
238
237
 
239
238
  - **设置页路径**:设置 → **订阅用量** → 提供商管理。「没有检测到API的默认隐藏」默认开启;首次没有可见条目时管理区自动展开。关闭的提供商仍可配置凭据,只是不会发起用量请求。
240
239
  - **凭据从哪来**:每一类凭据的**官网入口、环境变量名与界面填入位置**都写在[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md)——包括火山的 IAM AK/SK(子用户还要挂 `ArkReadOnlyAccess` 且不限制到项目)、MiMo 的平台会话 Cookie、以及各家 API Key 的对照表。**注意推理 Key 与额度凭据不通用。**
241
- - **默认开关策略**:新安装默认开启 10 条、默认关闭 17 条(**Codex 默认关闭**——它的提供方插件自带用量药丸);开关即时保存,升级保留已保存的开关,不重算默认值。默认隐藏只影响显示,不会自动开启被关闭的提供商。
240
+ - **默认开关策略**:新安装默认开启 11 条、默认关闭 17 条(**Codex 默认关闭**——它的提供方插件自带用量药丸);开关即时保存,升级保留已保存的开关,不重算默认值。默认隐藏只影响显示,不会自动开启被关闭的提供商。
242
241
  - **火山方舟是一组**:4 条路由共用同一组 IAM AK/SK,所以设置页只呈现**一张卡片、一个开关**(本机已装的第一条路由作为代表,都没装则用第一条),卡片里写明本机实际装了哪几条。这些 provider id 并没有合并——药丸仍按 id 匹配路由。旧插件 `@volcengine/ark-plan-api` 的两个国内路由(`ark-coding-plan-cn` / `ark-agent-plan-cn`)**已移除**:它们与 arkcli 的路由查同一份订阅,并列显示只会让人以为买了两个套餐;装了旧插件的用户,药丸仍按那两个 id 显示同一份数据(入口归并到 arkcli 路由)。
243
242
  - **火山方舟不按路由收起**:它的额度走**账号级管控面**,一组 AK/SK 就能查到名下所有套餐(`GetCodingPlanUsage` 与 `GetAFPUsage` 是同一套协议、同一组凭据)。推理路由装没装只影响模型能不能用,**不影响额度能不能查**——所以只持有 Coding Plan、却没在 DSH 里配 coding-plan 路由时,那条额度照样会显示(前提是配了 AK/SK)。
244
243
  - **凭据来源优先级**(`inherit` 模式):**凭据服务 → 启动环境 → 旧手动配置兜底**;切到自定义(`manual`)模式时只使用保存的手动 Key。界面会区分这三种来源。
@@ -253,7 +252,8 @@ dsh plugin --profile <profile> remove dsh-subusage
253
252
 
254
253
  | 模型商 | 默认继承变量 | 说明 |
255
254
  |---|---|---|
256
- | DeepSeek | `DEEPSEEK_API_KEY` | 余额型:`GET /user/balance` 返回账户余额,**与推理是同一把 Key** |
255
+ | DeepSeek API | `DEEPSEEK_API_KEY` | 余额型:`GET /user/balance` 返回账户余额,**与推理是同一把 Key** |
256
+ | DeepSeek 账号 | 无 | `deepseek-account` 路由;只读 DSH「账号与余额」中已登录账号的充值余额,**没有继承变量、也不使用独立 API Key**,请在 DSH 的账号与余额页面登录 |
257
257
  | Z.ai(中国) | `ZAI_CODING_CN_API_KEY` | 团队套餐另填组织、项目 ID(作为 `bigmodel-organization` / `bigmodel-project` 请求头) |
258
258
  | Z.ai(国际) | `ZAI_CODING_API_KEY` | 独立国际 Coding Plan Key,鉴权用裸 `authorization`,不带 `Bearer` |
259
259
  | Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开放平台 Key |
@@ -263,7 +263,7 @@ dsh plugin --profile <profile> remove dsh-subusage
263
263
  | MiniMax(中国) | `MINIMAX_CN_API_KEY` | `minimax-cn` 路由;中国站订阅 Key |
264
264
  | Command Code | `COMMANDCODE_API_KEY` | 凭据由提供方插件管理,本页只读继承;兜底读取 `~/.commandcode/auth.json`(`cmd login`) |
265
265
  | SuperGrok | 无 | `xai-oauth` 路由;读取 dsh-grok-kit / Grok CLI 共享的 OAuth 登录文件 `~/.grok/auth.json`(旧版 `~/.dsh/.xai-oauth-auth.json`),不保存凭据。access token 过期时**借 dsh-grok-kit(≥0.2.0)的官方登录链自动续约**——同一文件、同一把写锁、compare-and-write,不会顶掉共享登录态(本插件绝不自行裸 POST 刷新);未安装 grok-kit 或续约失败时才提示手动重新登录。API Key(`XAI_API_KEY`)取不到订阅周池 |
266
- | Codex | 无 | `openai-codex` 路由;只读 Codex CLI 的 ChatGPT 订阅登录文件(`$CODEX_HOME/auth.json` 或 `~/.codex/auth.json`,要求 `auth_mode` 为 `chatgpt`),不保存、不刷新 token |
266
+ | Codex | 无 | `openai-codex` 路由;0.13.0 起优先只读 Subscription 活动 OAuth 账号,再回退 CLI 文件(`$CODEX_HOME/auth.json` 或 `~/.codex/auth.json`,要求 `auth_mode` 为 `chatgpt`);不保存或刷新 token,续约由所属插件负责 |
267
267
  | 火山方舟 Ark(4 条路由) | `VOLC_ACCESSKEY` + `VOLC_SECRETKEY` | 额度查询要 **IAM Access Key 的 AK/SK 配对**,与推理 API Key(`ARKCLI_*_API_KEY` / `ARK_*_API_KEY`)**不是同一套**;4 条路由共用一组 AK/SK。这一组 AK/SK 能**同时**查到 Agent Plan 与 Coding Plan(含团队版席位)——不存在「Coding Plan 专用查询 key」 |
268
268
  | SiliconFlow | `SILICONFLOW_API_KEY` | 余额型:`GET /v1/user/info` 返回账户余额,**与推理是同一把 Key** |
269
269
  | OpenRouter | `OPENROUTER_API_KEY` | key 限额与用量用推理 key 即可;**账户余额需要 management / provisioning key**,普通 key 会被 403 拒——此时静默降级为只显示限额,不判失败 |
@@ -325,7 +325,7 @@ arkcli-coding-plan-team ARKCLI_CODING_PLAN_TEAM_API_KEY
325
325
  | `~/.grok/auth.json`(回退 `~/.dsh/.xai-oauth-auth.json`) | Grok CLI / dsh-grok-kit 的 OAuth 登录 | 平时只读;过期时经 grok-kit 官方链续约并写回同一文件,不自行裸刷 |
326
326
  | `~/.commandcode/auth.json` | Command Code CLI(`cmd login`)的 Key | **只读**兜底 |
327
327
 
328
- **网络访问**:只连上面清单里各家自己的额度 / 余额端点,以及 `platform.xiaomimimo.com`(MiMo 用量与登录)、`api.commandcode.ai`、`cli-chat-proxy.grok.com`、`chatgpt.com/backend-api`、火山管控面 `open.volcengineapi.com`,外加你自己填写的 LiteLLM proxy 地址。完整域名表见 [网络端点](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/providers.md#网络端点)。MiMo 自动登录会另外打开官方平台页面,由你在该页面自行输入密码与验证码。**本插件没有自己的服务器,不做遥测,也不把你的数据发给任何第三方。**
328
+ **网络访问**:只连上面清单里各家自己的额度 / 余额端点,以及 `platform.xiaomimimo.com`(MiMo 用量与登录)、`api.commandcode.ai`、`cli-chat-proxy.grok.com`、`chatgpt.com/backend-api`、火山管控面 `open.volcengineapi.com`,外加你自己填写的 LiteLLM proxy 地址。完整域名表见 [网络端点](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/providers.md#网络端点)。**`deepseek-account` 不自行发起 HTTP 请求**——它复用 DSH 的 `deepseekAccount` 服务,由宿主完成鉴权与余额请求(宿主服务可能按自身配置联网);本插件不读取、导出或保存 OAuth token。MiMo 自动登录会另外打开官方平台页面,由你在该页面自行输入密码与验证码。**本插件没有自己的服务器,不做遥测,也不把你的数据发给任何第三方。**
329
329
 
330
330
  **命令、进程与安装期行为**
331
331
 
@@ -442,7 +442,7 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
442
442
 
443
443
  文档入口:[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md)(每类凭据的官网入口、环境变量与填入位置、报错对照)、[开发与验证](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md)(RPC 契约、缓存与凭据约定、离线预览与实机验收范围)、[提供商清单与额度语义](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/providers.md)、[提供商覆盖与取舍](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/provider-coverage.md)、[UX 设计](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/design-ux.md)、[发布与市场收录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/publish.md)、[更新记录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/changelog.md)、[审查记录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/code-review.md)。
444
444
 
445
- > 打包说明:`package.json` 的 `files` 白名单是 `lib` / `cordis.patch.yml` / `README.md` / `LICENSE`,因此 `assets/title.svg`、`assets/screenshots/` 与 `docs/` **不进 npm 包**——图片通过 GitHub 绝对地址加载,npm 页面无需把图片打进包也能显示。改动白名单属于发布决策,需单独处理。
445
+ > 打包说明:`package.json` 的 `files` 白名单是 `lib` / `cordis.patch.yml` / `README.md` / `LICENSE`,因此 `assets/title.png`、`assets/readme/`、`assets/screenshots/` 与 `docs/` **不进 npm 包**——图片通过 GitHub 绝对地址加载,npm 页面无需把图片打进包也能显示。改动白名单属于发布决策,需单独处理。
446
446
 
447
447
  ### 界面预览
448
448
 
@@ -456,7 +456,16 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
456
456
 
457
457
  ![浅色 NanoGPT 用量弹层(离线测试预览)](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/usage-popover-light.png)
458
458
 
459
- 药丸 15 种状态一览(浅色 / 深色)见[开头的能力展示](#状态一眼分清)。
459
+ <details>
460
+ <summary>查看已发布基础药丸的 15 种状态(虚构数据)</summary>
461
+
462
+ ![浅色药丸状态](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-light.png)
463
+
464
+ ![深色药丸状态](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-dark.png)
465
+
466
+ </details>
467
+
468
+ 0.13.0 的功能特写见[功能预览](#功能预览)。
460
469
 
461
470
  截图声明见根目录 [`screenshots.json`](screenshots.json)。上述四张保留 0.10.11 的基础界面预览;下方三张为 0.11.2 新增语义状态的真实组件离线预览(虚构用量、桩网络、无凭据),底部水印明确标注「非运行中的 DSH 截图」。真实 DSH Loader、实际账号在线接口与多账户映射仍未验收,截图不代表已通过这些检查。
462
471
 
package/lib/client.js CHANGED
@@ -62,6 +62,8 @@ window.__ModuleLoader__.load({
62
62
  unavailable: "暂不可用",
63
63
  planLabel: "套餐",
64
64
  balanceLabel: "余额",
65
+ balanceLow: "余额偏低",
66
+ pillNeedDeepseekAccount: "请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key。",
65
67
  spendLabel: "已用",
66
68
  monthlyBalanceLabel: "月额度剩余",
67
69
  planSnapshotLimit: "月总额来自套餐快照,不含已购/赠送余额;未知套餐不推算上限",
@@ -153,8 +155,9 @@ window.__ModuleLoader__.load({
153
155
  ccSwitchFailed: "切换失败",
154
156
  // Command Code 自动轮换池健康度(其余账户用量由 Host 并行读取,各自缓存 60 秒)。
155
157
  ccPoolTitle: "轮换账户池",
158
+ ccPoolDetails: "点击查看详情", ccPoolWindowLimited: "{windows}额度已用尽", ccPoolLimitUnknown: "额度受限(周期未知)", ccPoolPeriodUnknown: "未知周期",
156
159
  ccPoolAllHealthy: "{n} 个账户均可轮换使用",
157
- ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个已达限额",
160
+ ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个账户有额度窗口受限",
158
161
  ccPoolUnusable: "{unusable} 个账户无可用凭据或暂时读不到",
159
162
  ccPoolExhausted: "轮换账户均已达限额,新请求很可能被拒绝",
160
163
  ccPoolExhaustedShort: "池已满限额",
@@ -218,6 +221,8 @@ window.__ModuleLoader__.load({
218
221
  unavailable: "Unavailable",
219
222
  planLabel: "Plan",
220
223
  balanceLabel: "Balance",
224
+ balanceLow: "Low balance",
225
+ pillNeedDeepseekAccount: "Sign in under DSH Settings → Account & balance; this entry does not use an API key.",
221
226
  spendLabel: "Spent",
222
227
  monthlyBalanceLabel: "Monthly credits remaining",
223
228
  planSnapshotLimit: "Monthly total uses a plan snapshot, excluding purchased/free credits; unknown plans have no estimated cap",
@@ -308,8 +313,9 @@ window.__ModuleLoader__.load({
308
313
  ccSwitchFailed: "Switch failed",
309
314
  // Command Code rotation-pool health (the Host reads the other accounts in parallel, each cached 60s).
310
315
  ccPoolTitle: "Rotation account pool",
316
+ ccPoolDetails: "Click to view details", ccPoolWindowLimited: "{windows} quota used up", ccPoolLimitUnknown: "Quota restricted (period unknown)", ccPoolPeriodUnknown: "Unknown period",
311
317
  ccPoolAllHealthy: "All {n} accounts are available",
312
- ccPoolCapacity: "{available}/{total} accounts available, {blocked} at their limit",
318
+ ccPoolCapacity: "{available}/{total} accounts available, {blocked} with limited quota windows",
313
319
  ccPoolUnusable: "{unusable} account(s) have no usable credentials or could not be read",
314
320
  ccPoolExhausted: "Every rotation account is at its limit; new requests are likely to be rejected",
315
321
  ccPoolExhaustedShort: "pool at limit",
@@ -335,7 +341,8 @@ window.__ModuleLoader__.load({
335
341
  const KIND_SHORT = { "5h": "s5h", "7d": "s7d", day: "sday", week: "sweek", month: "smonth", period: "speriod", sub: "ssub", rolling: "srolling" };
336
342
  const PROVIDER_META = {
337
343
  // 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
338
- "deepseek": { label: "DeepSeek", short: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
344
+ "deepseek": { label: "DeepSeek API", short: "DeepSeek API", envName: "DEEPSEEK_API_KEY" },
345
+ "deepseek-account": { label: "DeepSeek 账号", short: "DeepSeek 账号", envName: "", managedByPlugin: true },
339
346
  "zai-coding": { label: "Z.ai Coding (International)", short: "Z.ai Intl", envName: "ZAI_CODING_API_KEY", defaultEnabled: false },
340
347
  "synthetic": { label: "Synthetic", short: "Synthetic", envName: "SYNTHETIC_API_KEY", defaultEnabled: false },
341
348
  "nanogpt": { label: "NanoGPT", short: "NanoGPT", envName: "NANOGPT_API_KEY", defaultEnabled: false },
@@ -383,7 +390,7 @@ window.__ModuleLoader__.load({
383
390
  "litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
384
391
  };
385
392
  // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
386
- const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
393
+ const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek-account", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
387
394
  // 火山方舟的 4 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
388
395
  // 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
389
396
  const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
@@ -394,7 +401,7 @@ window.__ModuleLoader__.load({
394
401
  * 但装了旧插件的用户,模型路由仍然是这两个 id —— 药丸必须在入口把它们归并到 arkcli 路由,
395
402
  * 否则那些用户会突然失去用量显示。(第三条 BytePlus 已随实现一并移除:无 BytePlus 账号可验证。)
396
403
  */
397
- const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan" };
404
+ const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan", "deepseek-official": "deepseek-account" };
398
405
  function providerEnabled(settings, id) {
399
406
  if (PROVIDER_META[id]?.volc) {
400
407
  // 组开关优先;旧配置里没有它时按「组内任一条开着」合并过来,全新配置则沿用默认。
@@ -408,6 +415,11 @@ window.__ModuleLoader__.load({
408
415
  // 默认收起比「常量里默认开启」更贴近事实。undefined = 拿不到路由表,按「未知」放行,不谎报未安装。
409
416
  // 例外是火山方舟:它的额度走账号级管控面(一组 AK/SK 覆盖名下所有套餐),路由只是推理入口 —— 见 providerVisible。
410
417
  function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
418
+ const BALANCE_ONLY_IDS = new Set(["deepseek", "deepseek-account", "siliconflow", "novita", "hyperbolic", "deepinfra", "vercel-ai-gateway"]);
419
+ function providerCanQuery(result, id) {
420
+ const detected = id === "openai-codex" && result?.entries?.find(entry => entry.providerId === id)?.apiDetected === true;
421
+ return BALANCE_ONLY_IDS.has(id) || !!PROVIDER_META[id]?.volc || detected || !!result?.settings?.hasKeys?.[id] || result?.settings?.keyModes?.[id] === "manual" || providerRoutable(result, id);
422
+ }
411
423
  function providerVisible(settings, id, entry, result) {
412
424
  if (!providerEnabled(settings, id)) return false;
413
425
  // 关掉「没有检测到 API 的默认隐藏」就是要看全部,不再按路由、凭据或订阅状态收起。
@@ -418,7 +430,9 @@ window.__ModuleLoader__.load({
418
430
  // `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
419
431
  if (PROVIDER_META[id]?.volc) {
420
432
  if (entry?.subscribed === false) return false;
421
- } else if (!providerRoutable(result, id)) return false;
433
+ } else if (!providerCanQuery(result, id)) return false;
434
+ // 官方余额与本机账号余额独立于模型路由;刚保存 Key 的旧 no-key 条目不能继续隐藏。
435
+ if (settings?.hasKeys?.[id]) return true;
422
436
  return entry?.apiDetected !== false && !["no-key", "no-cookie"].includes(entry?.state);
423
437
  }
424
438
  /** token 数紧凑格式:1,234,567,890 → 1.23B */
@@ -471,9 +485,37 @@ window.__ModuleLoader__.load({
471
485
  return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : ["note", "quota-status"].includes(x.kind) ? t("noteLabel") : x.kind === "credit-status" ? t("creditStatusLabel") : x.kind === "spend" || x.kind.endsWith("-spend") ? t("spendLabel") : t("balanceLabel");
472
486
  }
473
487
  /** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
488
+ function formatBalanceValue(value) {
489
+ if (typeof value !== "string") return value;
490
+ const match = value.trim().match(/^([¥¥$]?\s*)([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(\s*(?:[A-Z]{3}|credits)?)$/i);
491
+ if (!match || !Number.isFinite(Number(match[2]))) return value;
492
+ if (!/[eE]/.test(match[2]) && (match[2].split(".")[1]?.length ?? 0) <= 2) return value.trim();
493
+ const amount = Number(match[2]);
494
+ // 仅收敛展示精度;状态阈值仍使用未经四舍五入的原始金额。
495
+ const formatted = String(Number(amount.toFixed(2)));
496
+ return match[1] + formatted + match[3];
497
+ }
474
498
  function balanceText(entry, t) {
475
499
  const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
476
- return row ? `${extraLabel(row, t)} ${row.value}` : null;
500
+ return row ? `${extraLabel(row, t)} ${row.kind === "balance" ? formatBalanceValue(row.value) : row.value}` : null;
501
+ }
502
+ /** 余额只按自己的单位预警,不做汇率折算,也不把累计消费当余额。 */
503
+ function balanceVisual(entry, t) {
504
+ const row = Array.isArray(entry?.extras) ? entry.extras.find(x => x.kind === "balance" && typeof x.value === "string") : undefined;
505
+ if (!row) return null;
506
+ const text = row.value.trim();
507
+ const decimal = "([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?)";
508
+ const suffix = text.match(new RegExp("^" + decimal + "\\s+([A-Z]{3}|credits)$", "i"));
509
+ const prefix = text.match(new RegExp("^([¥¥$])\\s*" + decimal + "$", "i"));
510
+ const bare = text.match(new RegExp("^" + decimal + "$", "i"));
511
+ const amount = suffix ? Number(suffix[1]) : prefix ? Number(prefix[2]) : bare ? Number(bare[1]) : NaN;
512
+ const unit = suffix ? suffix[2].toUpperCase() : prefix ? prefix[1] === "$" ? "USD" : "CNY" : null;
513
+ const rawAmount = suffix?.[1] ?? prefix?.[2];
514
+ const value = formatBalanceValue(unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text);
515
+ // 产品预警阈值,不是汇率或能运行多少次的保证。credits 没有跨厂商统一价值。
516
+ const limits = unit === "CNY" ? { red: 1, amber: 5 } : unit === "USD" ? { red: 0.2, amber: 1 } : null;
517
+ const rank = Number.isFinite(amount) ? amount <= 0 || limits && amount < limits.red ? 2 : limits && amount < limits.amber ? 1 : 0 : 0;
518
+ return { value, color: rank === 2 ? "#ef4444" : rank === 1 ? "#eab308" : limits ? "#22c55e" : "#94a3b8", icon: rank > 0 ? "!" : limits ? "✓" : "ℹ", tinted: rank > 0, label: rank > 0 ? t("balanceLow") : "" };
477
519
  }
478
520
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
479
521
  function durationText(ms, t) {
@@ -751,7 +793,7 @@ window.__ModuleLoader__.load({
751
793
  /** 每次 apply 一个 store;partial response 只合并请求项,所有槽共享 inflight 与活动计时。 */
752
794
  function createUsageStore(call) {
753
795
  let snapshot = { updatedAt: null, settings: null, configured: {}, entries: [] };
754
- let disposed = false, initial = null, timer = null, epoch = 0, sequence = 0, metadataSequence = 0;
796
+ let disposed = false, initial = null, settingsInitial = null, timer = null, epoch = 0, sequence = 0, metadataSequence = 0;
755
797
  let loginGeneration = 0, loginJobId = null, loginPending = false;
756
798
  // Command Code 显示账户:弹层账户区选中的账户("" = 默认账户/顶层 Key)。
757
799
  // 所有 commandcode 读取(readAll/refresh/定时刷新)都携带它,切换后用量同步跟随。
@@ -787,7 +829,7 @@ window.__ModuleLoader__.load({
787
829
  // 全部静默不发请求,用户点「刷新当前」也空转,只有「刷新全部」走 read 才能更新。
788
830
  const unique = [...new Set(ids)].filter((id) => {
789
831
  if (!PROVIDER_ORDER.includes(id) || !providerEnabled(snapshot.settings, id)) return false;
790
- return PROVIDER_META[id]?.volc ? true : providerRoutable(snapshot, id);
832
+ return providerCanQuery(snapshot, id);
791
833
  });
792
834
  const start = (id) => {
793
835
  if (inflight.has(id)) return inflight.get(id);
@@ -852,6 +894,15 @@ window.__ModuleLoader__.load({
852
894
  };
853
895
  return {
854
896
  subscribe, getSnapshot: () => snapshot, refresh, activate,
897
+ // 设置页先取本机元数据,不等待所有厂商的网络额度请求。
898
+ readSettings: () => {
899
+ if (snapshot.settings) return Promise.resolve(snapshot);
900
+ if (!settingsInitial) {
901
+ const request = markRequest([]);
902
+ settingsInitial = Promise.resolve().then(() => call("read", { providerIds: [], force: false, commandCodeAccount: ccAccount })).then(value => merge(value, request)).catch(error => { settingsInitial = null; throw error; });
903
+ }
904
+ return settingsInitial.then(() => snapshot);
905
+ },
855
906
  getCommandCodeAccount: () => ccAccount, setCommandCodeAccount,
856
907
  startMimoLogin: async (request) => { const generation = ++loginGeneration, startedEpoch = epoch; loginJobId = null; return acceptLogin(await call("startMimoLogin", request), generation, startedEpoch); },
857
908
  getMimoLoginStatus: async () => { const generation = loginGeneration, startedEpoch = epoch, jobId = loginPending ? loginJobId : null; return acceptLogin(await call("getMimoLoginStatus"), generation, startedEpoch, jobId); },
@@ -1058,12 +1109,16 @@ window.__ModuleLoader__.load({
1058
1109
  const worst = worstWindow(account.windows || []);
1059
1110
  const tier = account.availability === "paygo" ? { color: "#eab308", icon: "ℹ" } : account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
1060
1111
  const blocked = account.blocked;
1112
+ const limited = (account.windows || []).filter(w => w.status === "rate-limited");
1113
+ const roots = limited.filter(w => !w.cascade);
1114
+ const kinds = [...new Set(roots.length ? roots.map(w => w.kind) : limited.flatMap(w => w.detail?.blockedBy?.length ? w.detail.blockedBy : [w.kind]))];
1115
+ const limitDetail = kinds.length ? t("ccPoolWindowLimited").replace("{windows}", kinds.map(kind => t(KIND_LABEL[kind] || "ccPoolPeriodUnknown")).join(" / ")) : t("ccPoolLimitUnknown");
1061
1116
  const unusable = !account.usable && !account.blocked;
1062
1117
  const dot = statusGlyph(unusable ? NEUTRAL : (tier ? tier.color : NEUTRAL), unusable ? "?" : blocked ? "✕" : (tier ? tier.icon : "?"), 14);
1063
1118
  const detail = unusable
1064
1119
  ? (account.availability === "unknown" ? t("statusUnknownShort") : account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
1065
1120
  : blocked
1066
- ? t("pillLimited")
1121
+ ? limitDetail
1067
1122
  : account.availability === "paygo" ? t("statusPaygoShort") : worst ? pillText(worst, (account.windows || []).length > 1, t)
1068
1123
  : t("noUsageData");
1069
1124
  const canPin = cc && account.id !== "default";
@@ -1075,7 +1130,7 @@ window.__ModuleLoader__.load({
1075
1130
  },
1076
1131
  dot,
1077
1132
  h("span", { style: { flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, account.id === "default" ? t("ccAccountDefault") : account.label),
1078
- h("span", { style: { opacity: 0.75, whiteSpace: "nowrap" } }, detail));
1133
+ h("span", { style: { opacity: 0.75, flex: "0 1 60%", minWidth: 0, textAlign: "right", whiteSpace: "normal", overflowWrap: "anywhere" } }, detail));
1079
1134
  };
1080
1135
  return h("div", { style: { marginTop: 10, padding: "8px 10px", borderRadius: 10, border: `1px solid ${alpha(tone, ".35")}`, background: alpha(tone, ".07") } },
1081
1136
  h("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, fontWeight: 600, color: textTone(tone) } },
@@ -1084,7 +1139,9 @@ window.__ModuleLoader__.load({
1084
1139
  h("span", { style: { fontWeight: 500, opacity: 0.9 } }, `${pool.available}/${pool.total}`)),
1085
1140
  h("p", { role: pool.exhausted ? "alert" : "status", style: { margin: "3px 0 0", fontSize: 11, color: textTone(tone) } }, summary),
1086
1141
  pool.unusableCount > 0 ? h("p", { style: { margin: "2px 0 0", fontSize: 10.5, opacity: 0.7 } }, t("ccPoolUnusable").replace("{unusable}", pool.unusableCount)) : null,
1087
- h("div", { style: { marginTop: 4 } }, pool.accounts.map(rowFor)));
1142
+ h("details", { style: { marginTop: 4 } },
1143
+ h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } }, t("ccPoolDetails")),
1144
+ h("div", { style: { maxHeight: 240, overflowY: "auto", marginTop: 4 } }, pool.accounts.map(rowFor))));
1088
1145
  }
1089
1146
 
1090
1147
  // ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
@@ -1153,7 +1210,7 @@ window.__ModuleLoader__.load({
1153
1210
  const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
1154
1211
  return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
1155
1212
  };
1156
- const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
1213
+ const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : providerId === "deepseek-account" ? t("pillNeedDeepseekAccount") : t("pillNeedKey"))
1157
1214
  : state === "no-cookie" ? t("pillNeedCookie") : null;
1158
1215
  const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
1159
1216
  if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
@@ -1165,6 +1222,8 @@ window.__ModuleLoader__.load({
1165
1222
  if (freshAdvice && rawEntry.availability === "blocked" && (rawEntry.availabilityReason !== "quota-exhausted" || !worstTier || worstTier.rank < 4)) return { ...base, key: "restricted", color: "#ef4444", icon: "✕", title: t("statusRestricted"), sub: "", short: t("statusRestrictedShort"), tinted: true };
1166
1223
  if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
1167
1224
  const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
1225
+ const wallet = !windows.length && rawEntry.coverage === "complete" && rawEntry.availability === "paygo" ? balanceVisual(rawEntry, t) : null;
1226
+ if (wallet && !expiryActionable) return { ...base, key: "paygo", color: wallet.color, icon: wallet.icon, title: balanceText(rawEntry, t), sub: [title, wallet.label].filter(Boolean).join(" · "), short: wallet.value, tinted: wallet.tinted };
1168
1227
  return { ...base, key: rawEntry.availability, color: "#eab308", icon: rawEntry.availability === "unknown" ? "?" : "ℹ", title, sub: rawEntry.availability === "paygo" ? t("statusPaygoSub") : worst ? windowSub(worst) : "", short: t(rawEntry.availability === "paygo" ? "statusPaygoShort" : rawEntry.availability === "included" ? "statusIncludedShort" : "statusUnknownShort"), tinted: true };
1169
1228
  }
1170
1229
  const scopeLimited = worstTier?.rank >= 4 && windows.some(w => w.status !== "rate-limited" && w.detail?.scope && w.detail.scope !== "all") && windows.filter(w => w.status === "rate-limited").every(w => w.detail?.scope && w.detail.scope !== "all");
@@ -1187,7 +1246,7 @@ window.__ModuleLoader__.load({
1187
1246
  // 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
1188
1247
  const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
1189
1248
  const worse = worstTier && worstTier.rank >= 3;
1190
- return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: label, tinted: true };
1249
+ return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: [!worst ? balanceVisual(entry, t)?.value : null, label].filter(Boolean).join(" · "), tinted: true };
1191
1250
  }
1192
1251
  if (worstTier) {
1193
1252
  const sub = windowSub(worst);
@@ -1197,8 +1256,8 @@ window.__ModuleLoader__.load({
1197
1256
  }
1198
1257
  if (entry.extras?.some(e => e.kind === "quota-status" && e.value === "unlimited")) return { ...base, key: "unlimited", color: NEUTRAL, icon: "ℹ", title: t("statusUnlimited"), sub: "", short: t("statusUnlimited"), tinted: false };
1199
1258
  // 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
1200
- const balance = balanceText(entry, t);
1201
- return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
1259
+ const balance = balanceText(entry, t), visual = balanceVisual(entry, t);
1260
+ return { ...base, key: "balance", color: visual?.color ?? NEUTRAL, icon: visual?.icon ?? "ℹ", title: balance ?? t("noUsageData"), sub: visual?.label ?? "", short: visual?.value ?? balance ?? t("noUsageData"), tinted: visual?.tinted ?? false };
1202
1261
  }
1203
1262
  /** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
1204
1263
  function statusBanner(status) {
@@ -1370,10 +1429,10 @@ window.__ModuleLoader__.load({
1370
1429
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1371
1430
  // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
1372
1431
  // 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
1373
- const tip = `${label}: ${st.title}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
1432
+ const tip = `${label}: ${st.title}${["balance", "paygo"].includes(st.key) && st.sub ? " · " + st.sub : ""}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
1374
1433
  // 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
1375
1434
  // 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
1376
- const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
1435
+ const glyph = windows.length ? miniBars(windows, degraded) : st.icon === "" ? null : statusGlyph(st.color, st.icon, 16);
1377
1436
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
1378
1437
  h("button", {
1379
1438
  type: "button",
@@ -1386,7 +1445,7 @@ window.__ModuleLoader__.load({
1386
1445
  display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
1387
1446
  borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
1388
1447
  background: st.tinted ? alpha(st.color, ".10") : "transparent",
1389
- color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
1448
+ color: ["balance", "paygo"].includes(st.key) && st.tinted ? `color-mix(in srgb, ${st.color} 65%, currentColor)` : "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
1390
1449
  }
1391
1450
  },
1392
1451
  glyph,
@@ -1430,7 +1489,7 @@ window.__ModuleLoader__.load({
1430
1489
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1431
1490
  entry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
1432
1491
  h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1433
- h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
1492
+ h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.kind === "balance" ? formatBalanceValue(x.value) : x.value))))
1434
1493
  : null,
1435
1494
  providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1436
1495
  // 页脚:更新时间 + 刷新。
@@ -1526,14 +1585,14 @@ window.__ModuleLoader__.load({
1526
1585
  const text = (cn, en) => local ? cn : en;
1527
1586
  react.useEffect(() => {
1528
1587
  let alive = true;
1529
- void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1588
+ void (usageStore.readSettings ?? usageStore.readAll)().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1530
1589
  return () => { alive = false; };
1531
1590
  }, [usageStore]);
1532
1591
  react.useEffect(() => {
1533
1592
  let alive = true;
1534
1593
  const enabled = editorOpen && providerEnabled(result.settings, tab);
1535
1594
  const deactivate = enabled ? usageStore.activate(tab) : () => {};
1536
- void usageStore.readAll().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1595
+ void (usageStore.readSettings ?? usageStore.readAll)().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1537
1596
  return () => { alive = false; deactivate(); };
1538
1597
  }, [usageStore, tab, editorOpen, providerEnabled(result.settings, tab)]);
1539
1598
  react.useEffect(() => {
@@ -1593,7 +1652,7 @@ window.__ModuleLoader__.load({
1593
1652
  };
1594
1653
  const refresh = async (all) => {
1595
1654
  setRefreshing(true);
1596
- try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
1655
+ try { if (!result.settings) await (usageStore.readSettings ?? usageStore.readAll)(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
1597
1656
  catch (error) { if (aliveRef.current) setLoadError(localError(error.code, String(error.message || error), t)); }
1598
1657
  finally { if (aliveRef.current) setRefreshing(false); }
1599
1658
  };
@@ -1702,7 +1761,7 @@ window.__ModuleLoader__.load({
1702
1761
  const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
1703
1762
  : tab === "openai-codex" ? text("来自 $CODEX_HOME/auth.json 或 ~/.codex/auth.json(codex login)", "From $CODEX_HOME/auth.json or ~/.codex/auth.json (codex login)")
1704
1763
  : text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)");
1705
- 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 };
1764
+ const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel, "account-service": text("本机 DeepSeek 账号余额", "Local DeepSeek account balance"), "subscription-credentials": text("Codex Subscription 活动账号(DSH 凭据服务)", "Codex Subscription active account (DSH credentials)") };
1706
1765
  const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1707
1766
  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,
1708
1767
  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,
@@ -1718,11 +1777,12 @@ window.__ModuleLoader__.load({
1718
1777
  h("div", { style: { marginTop: 10 } },
1719
1778
  h("fieldset", { disabled: busy || !result.settings || typeof result.settings.revision !== "string", style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
1720
1779
  tab === "zai-coding" ? h("p", null, text("使用 Z.ai 国际站 Coding Plan Key;与中国站凭据独立。", "Use an international Z.ai Coding Plan key; credentials are separate from the China region.")) : null,
1780
+ tab === "deepseek" ? h("p", null, text("此入口只读取独立 API Key 的官方余额,不使用 DSH 账号或 OAuth。可继承 DEEPSEEK_API_KEY,或手动填写官方开放平台 API Key;余额查询不要求配置模型路由。", "This entry reads only the independent API-key balance, never the DSH account or OAuth. Inherit DEEPSEEK_API_KEY or enter an official platform API key; no model route is required.")) : null,
1721
1781
  tab === "synthetic" ? h("p", null, text("使用 Synthetic API Key,仅显示模型订阅请求额度。", "Use a Synthetic API key. Shows the model subscription request quota.")) : null,
1722
1782
  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,
1723
1783
  h("p", null, `${result.configured && result.configured[tab] ? t("detected") : t("notDetected")} · ${sources[source] || sources.none}`),
1724
- 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,
1725
- 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,
1784
+ tab === "deepseek-account" ? h("p", null, text("只读 DSH「账号与余额」中已登录账号的充值余额。默认开启;不读取、导出或保存 OAuth token,不使用独立 API Key。请在 DSH 的账号与余额页面登录。", "Reads the signed-in account's recharge balance from DSH Account & balance. Enabled by default; never reads, exports or stores OAuth tokens, and never uses an independent API key. Sign in on DSH Account & balance.")) : tab === "commandcode" ? h(react.Fragment, null, h("p", null, t("commandcodeManaged"))) : tab === "xai-oauth" ? h(react.Fragment, null, h("p", null, t("grokManaged"))) : tab === "openai-codex" ? h(react.Fragment, null,
1785
+ h("p", null, text("优先只读 Codex Subscription 的独立 OAuth 活动账号,无需 CLI;没有该来源再读 Codex CLI 的 ChatGPT 订阅登录($CODEX_HOME/auth.json 或 ~/.codex/auth.json)。本页不保存凭据、不刷新 token;续约由凭据所属插件负责。请在 Subscription 设置页登录,或用 codex login;API Key 模式(auth_mode 不是 chatgpt)取不到订阅额度。", "Reads the Codex Subscription active OAuth account first (no CLI required), then the Codex CLI ChatGPT login ($CODEX_HOME/auth.json or ~/.codex/auth.json). This page never saves or refreshes tokens; renewal belongs to the owning plugin. Sign in in Subscription settings or with codex login; API-key mode (auth_mode not chatgpt) cannot read subscription quota."))) : PROVIDER_META[tab].volc ? h(react.Fragment, null,
1726
1786
  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.")),
1727
1787
  // 用户通常已经登录着控制台,直接给入口比让他自己翻菜单快得多。
1728
1788
  h("p", { style: { fontSize: 12 } }, externalLink("https://console.volcengine.com/iam/keymanage", text("打开火山引擎「API 访问密钥」创建 AK/SK", "Open Volcengine “API access keys”")), " · ", credentialsGuide),
@@ -1789,7 +1849,7 @@ window.__ModuleLoader__.load({
1789
1849
  return h("div", { key: id, style: { gridColumn: editing ? "1 / -1" : undefined, display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", border: "1px solid rgba(128,128,128,.25)", borderRadius: 8, padding: 8 } },
1790
1850
  h("span", { style: { flex: 1, minWidth: 80 } }, title),
1791
1851
  h("button", { type: "button", role: "switch", "aria-checked": enabled, disabled: busy || dirty || !result.settings?.revision, "aria-label": `${text("启用", "Enable")} ${title}`, style: enabled ? primary : btnStyle, onClick: () => saveVisibility(isArk ? { ark: !enabled } : { providers: { [id]: !enabled } }) }, enabled ? text("已开启", "On") : text("已关闭", "Off")),
1792
- h("span", { style: { width: "100%", fontSize: 12, opacity: .75 } }, arkNote ? arkNote + " · " : "", !enabled ? text("检测已关闭", "Detection disabled") : !providerRoutable(result, id) ? text("本机没装这条路由:提供方插件或 CLI 没有注册它,开了也读不到东西", "Route not installed on this machine: no provider plugin or CLI registered it, so enabling it reads nothing") : e?.apiDetected === false || ["no-key", "no-cookie"].includes(e?.state) ? text(hideWithoutApi ? "未检测到 API · 默认隐藏" : "未检测到 API", "No API credentials detected") : e?.apiDetected === true || (e?.keySource && e.keySource !== "none") ? text("已检测到 API / 凭据", "API credentials detected") : text("待检测", "Detection pending")),
1852
+ h("span", { style: { width: "100%", fontSize: 12, opacity: .75 } }, arkNote ? arkNote + " · " : "", !enabled ? text("检测已关闭", "Detection disabled") : !providerCanQuery(result, id) ? text("本机没装这条路由:提供方插件或 CLI 没有注册它,开了也读不到东西", "Route not installed on this machine: no provider plugin or CLI registered it, so enabling it reads nothing") : e?.apiDetected === false || ["no-key", "no-cookie"].includes(e?.state) ? text(hideWithoutApi ? "未检测到 API · 默认隐藏" : "未检测到 API", "No API credentials detected") : e?.apiDetected === true || (e?.keySource && e.keySource !== "none") ? text("已检测到 API / 凭据", "API credentials detected") : text("待检测", "Detection pending")),
1793
1853
  h("details", { id: `subusage-credentials-${id}`, open: editing, style: { width: "100%" } },
1794
1854
  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")),
1795
1855
  tab === id ? credentialEditor : null));
@@ -1852,7 +1912,7 @@ window.__ModuleLoader__.load({
1852
1912
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1853
1913
  usageEntry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
1854
1914
  h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1855
- h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
1915
+ h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.kind === "balance" ? formatBalanceValue(x.value) : x.value))))
1856
1916
  : null,
1857
1917
  // 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
1858
1918
  // 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
@@ -1922,7 +1982,12 @@ window.__ModuleLoader__.load({
1922
1982
  /** 槽包装:先看当前模型的 provider,再分发到对应药丸。 */
1923
1983
  function SubusagePillEntry({ store, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
1924
1984
  const state = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1925
- const provider = state.current && state.current.provider;
1985
+ const lastProvider = react.useRef(null);
1986
+ const currentProvider = state.current?.provider;
1987
+ // 宿主模型目录在重连/重建期间会以 loading/error + current:null 发布快照。
1988
+ // 只在这类暂态保留已知选择;ready 的空选择或显式换厂商不能沿用。
1989
+ if (currentProvider || !["loading", "error"].includes(state.status)) lastProvider.current = currentProvider ?? null;
1990
+ const provider = currentProvider ?? (["loading", "error"].includes(state.status) ? lastProvider.current : null);
1926
1991
  // 旧插件 @volcengine/ark-plan-api 的两个国内路由 id 不在本表里,但它们是**同一个 plan**:
1927
1992
  // 不管模型路由由哪个插件提供,都归并到 arkcli 路由再查——所以只会有**一个**面板,
1928
1993
  // 数据来自同一份订阅(实测两条路由的请求 URL 逐字相同)。
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // dsh-subusage Host:秘密仅保存在 Host,远端只暴露 provider 补丁和公开设置。
2
2
  import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
- import { credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
4
+ import { credentialKey, credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
5
5
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
6
6
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
7
  import { createHash } from "node:crypto";
@@ -11,7 +11,7 @@ import { MimoLogin } from "./mimo-login.js";
11
11
  import { parseVolcAgentPlan, parseVolcCodingPlan, parseVolcSeatAfp, parseVolcSeatCoding, parseVolcSeatIds, volcError, volcSignature } from "./volcengine.js";
12
12
 
13
13
  const name = "dsh-subusage";
14
- const VERSION = "0.11.2";
14
+ const VERSION = "0.13.0";
15
15
  const inject = ["llm"];
16
16
  const PROVIDERS = {
17
17
  "zai-coding-cn": { label: "Z.ai", envName: "ZAI_CODING_CN_API_KEY" },
@@ -64,7 +64,8 @@ const PROVIDERS = {
64
64
  // 国内那两个 id 药丸仍认:Client 的 ROUTE_PROVIDER_ALIAS 把它们归并到 arkcli 路由。
65
65
  // DeepSeek 官方:余额型(不是订阅窗口),与推理**同一把** API Key,端点与响应见官方文档
66
66
  // https://api-docs.deepseek.com/zh-cn/api/get-user-balance
67
- "deepseek": { label: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
67
+ "deepseek": { label: "DeepSeek API", envName: "DEEPSEEK_API_KEY" },
68
+ "deepseek-account": { label: "DeepSeek 账号", envName: "", managedByPlugin: true },
68
69
  // OpenRouter:key 限额与用量走 /key(任意 key 可用),账户余额走 /credits(**只对
69
70
  // management / provisioning key 开放**,普通推理 key 会被拒 → 静默降级为只显示限额)。
70
71
  "openrouter": { label: "OpenRouter", envName: "OPENROUTER_API_KEY", defaultEnabled: false },
@@ -112,6 +113,7 @@ const PROVIDER_ALIASES = {
112
113
  minimax: ["minimax-cn", "minimax"], "海螺": ["minimax-cn", "minimax"],
113
114
  commandcode: ["commandcode"], "command-code": ["commandcode"], "command code": ["commandcode"],
114
115
  supergrok: ["xai-oauth"], grok: ["xai-oauth"], xai: ["xai-oauth"],
116
+ "deepseek-official": ["deepseek-account"], "deepseek api": ["deepseek"], "deepseek 账号": ["deepseek-account"],
115
117
  codex: ["openai-codex"], chatgpt: ["openai-codex"], gpt: ["openai-codex"], openai: ["openai-codex"],
116
118
  ark: ARK_IDS, volcengine: ARK_IDS, volc: ARK_IDS, "火山": ARK_IDS, "方舟": ARK_IDS, "火山方舟": ARK_IDS, doubao: ARK_IDS, "豆包": ARK_IDS
117
119
  };
@@ -265,6 +267,30 @@ function grokAuthFileCredentials(io) {
265
267
  // 与 Grok 一样只读:token 由 Codex CLI 自己轮换,第三端刷新会互相顶掉。
266
268
  const CODEX_AUTH_DIR = join(homedir(), ".codex");
267
269
  const CODEX_API_BASE = "https://chatgpt.com/backend-api";
270
+ // dsh-codex-subscription 的活动账号记录是权威来源;只读,不迁移或写回第三方 OAuth。
271
+ export async function codexSubscriptionCredentials(credentials) {
272
+ if (!credentials) return undefined;
273
+ const normalize = value => {
274
+ if (value?.type !== "oauth" || typeof value.access !== "string" || !value.access.trim() || typeof value.accountId !== "string" || !value.accountId.trim() || !Number.isFinite(value.expires)) throw failure("subusage/credential", "Codex Subscription 登录凭据格式异常,请在该插件设置页检查登录");
275
+ return { key: value.access, accountId: value.accountId, expiresAt: value.expires, source: "subscription-credentials" };
276
+ };
277
+ const record = await credentials.readRecord?.(credentialKey("codex-subscription", "accounts"));
278
+ if (record !== undefined) {
279
+ const payload = record?.payload;
280
+ if (record?.kind !== "grant" || payload?.version !== 1 || typeof payload.activeId !== "string" || !Array.isArray(payload.accounts)) throw failure("subusage/credential", "Codex Subscription 账号记录格式异常");
281
+ const selected = payload.accounts.filter(account => account?.id === payload.activeId);
282
+ if (selected.length !== 1) throw failure("subusage/credential", "Codex Subscription 活动账号不存在或重复");
283
+ return normalize(selected[0].credential);
284
+ }
285
+ for (const name of ["OPENAI_CODEX_SUBSCRIPTION_OAUTH", "WSL043_OPENAI_CODEX_OAUTH"]) {
286
+ const hit = await credentials.resolve?.(credentialRef(name));
287
+ if (!hit?.value) continue;
288
+ let value;
289
+ try { value = JSON.parse(hit.value); } catch { throw failure("subusage/credential", "Codex Subscription 登录凭据不是有效 JSON"); }
290
+ return normalize(value);
291
+ }
292
+ return undefined;
293
+ }
268
294
  export function codexAuthFileCredentials(io, env) {
269
295
  const home = typeof env?.CODEX_HOME === "string" && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : CODEX_AUTH_DIR;
270
296
  try {
@@ -518,18 +544,25 @@ export function normalizeZai(body) {
518
544
  return { windows: cascadeRateLimited(windows), level: typeof data.level === "string" ? data.level : undefined };
519
545
  }
520
546
  export function normalizeMimo(balBody, detailBody, usageBody) {
521
- const bal = balBody?.data;
522
- if (!bal || typeof bal.balance !== "string" || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(bal.balance) || !Number.isFinite(Number(bal.balance))) throw failure("subusage/response", "Invalid MiMo balance response");
547
+ const rawBal = balBody?.data;
548
+ const bal = rawBal && typeof rawBal.balance === "string" && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(rawBal.balance) && Number.isFinite(Number(rawBal.balance)) ? rawBal : undefined;
523
549
  const data = usageBody?.data ?? {}; const detail = detailBody?.data ?? {};
524
550
  const pick = (group, name) => Array.isArray(group?.items) ? group.items.find(i => i?.name === name) : undefined;
525
- const pool = pick(data.usage, "plan_total_token") ?? pick(data.monthUsage, "month_total_token");
526
- const poolDetail = numeric(pool?.used) && numeric(pool?.limit) ? { used: pool.used, limit: pool.limit, unit: "credits" } : undefined;
527
- const windows = pool && numeric(pool.percent) ? [windowRow("sub", pool.percent * 100, detail.currentPeriodEnd, undefined, poolDetail)] : [];
528
- const currency = typeof bal.currency === "string" && /^[A-Z]{3}$/.test(bal.currency) ? bal.currency : "";
529
- const extras = [{ kind: "balance", value: `${bal.balance} ${currency}`.trim() }];
551
+ const pools = [pick(data.usage, "plan_total_token"), pick(data.monthUsage, "month_total_token")].filter(Boolean);
552
+ const pool = pools.find(row => row.limit !== 0) ?? pools[0];
553
+ const groups = ["usage", "monthUsage"].filter(key => Object.hasOwn(data, key)).map(key => data[key]);
554
+ const emptyGroups = groups.length > 0 && groups.every(group => group === null || Array.isArray(group?.items) && group.items.length === 0);
555
+ const detailReadable = detailBody?.data === null || detailBody?.data && typeof detailBody.data === "object" && !Array.isArray(detailBody.data);
556
+ const noSubscription = detailReadable && [undefined, 0].includes(detailBody?.code) && [undefined, 0].includes(usageBody?.code) && !detail.planName && !detail.planCode && (emptyGroups || pools.length > 0 && pools.every(row => row.limit === 0 && row.used === 0 && row.percent === 0));
557
+ const poolDetail = numeric(pool?.used) && numeric(pool?.limit) && pool.limit > 0 ? { used: pool.used, limit: pool.limit, unit: "credits" } : undefined;
558
+ const windows = pool && pool.limit !== 0 && numeric(pool.percent) ? [windowRow("sub", pool.percent * 100, detail.currentPeriodEnd, undefined, poolDetail)] : [];
559
+ const currency = typeof bal?.currency === "string" && /^[A-Z]{3}$/.test(bal.currency) ? bal.currency : "";
560
+ const extras = bal ? [{ kind: "balance", value: `${bal.balance} ${currency}`.trim() }] : [];
561
+ if (!bal && !windows.length) throw failure("subusage/response", "MiMo 没有可读的余额或套餐用量");
562
+ if (!bal) extras.push({ kind: "note", value: "余额暂不可读;套餐用量已更新" });
530
563
  const plan = detail.planName || detail.planCode;
531
564
  if (typeof plan === "string") extras.push({ kind: "plan", value: plan });
532
- return { windows, extras, balanceOk: true, coverage: windows.length ? "complete" : "partial" };
565
+ return { windows, extras, balanceOk: !!bal, ...(windows.length ? { subscribed: true } : noSubscription ? { subscribed: false } : {}), coverage: windows.length || noSubscription && !!bal ? "complete" : "partial" };
533
566
  }
534
567
  export function normalizeOpencodeGo(body) {
535
568
  const src = body?.usage ?? body;
@@ -734,7 +767,7 @@ export function normalizeCodexUsage(body) {
734
767
  const credits = body?.credits && typeof body.credits === "object" && !Array.isArray(body.credits) ? body.credits : {};
735
768
  const balance = typeof credits.balance === "string" && credits.balance.trim() ? credits.balance.trim() : typeof credits.balance === "number" ? String(credits.balance) : "";
736
769
  const creditBalance = balance && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(balance) && Number.isFinite(Number(balance)) ? Number(balance) : undefined;
737
- if (credits.has_credits === true && credits.unlimited !== true && creditBalance !== undefined && creditBalance > 0) extras.push({ kind: "balance", value: balance });
770
+ if (credits.has_credits === true && credits.unlimited !== true && creditBalance !== undefined) extras.push({ kind: "balance", value: balance });
738
771
  if (credits.has_credits === true && credits.unlimited === true) extras.push({ kind: "credit-status", value: "unlimited" });
739
772
  const control = body?.spend_control;
740
773
  const member = control?.individual_limit;
@@ -759,6 +792,15 @@ export function normalizeCodexUsage(body) {
759
792
  // DeepSeek 官方余额(余额型):GET https://api.deepseek.com/user/balance,Bearer 与推理同一把 Key。
760
793
  // 响应 { is_available, balance_infos: [{ currency, total_balance, granted_balance, topped_up_balance }] },
761
794
  // 金额是**字符串十进制**。余额没有上限也就没有百分比:不产出窗口,只产出 extras。
795
+ // DSH 账号服务已完成鉴权与余额响应投影;本插件不读取账号 grant/token。
796
+ export function normalizeDeepseekAccountBalance(body) {
797
+ const decimal = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
798
+ if (body?.status !== "ready" || !Array.isArray(body.value)) throw failure("subusage/response", "本机 DeepSeek 账号余额读取失败,请检查 DSH 的账号与余额页面", true);
799
+ const wallets = body.value.filter(row => ["CNY", "USD"].includes(row?.currency) && typeof row.balance === "string" && decimal.test(row.balance));
800
+ if (!wallets.length || wallets.length !== body.value.length) throw failure("subusage/response", "本机 DeepSeek 账号没有有效的充值余额信息");
801
+ const row = wallets.find(row => row.currency === "CNY") ?? wallets[0];
802
+ return { windows: [], extras: [{ kind: "balance", value: `${row.balance} ${row.currency}` }], coverage: "complete" };
803
+ }
762
804
  export function normalizeDeepseekBalance(body) {
763
805
  if (typeof body?.is_available !== "boolean") throw failure("subusage/response", "Invalid DeepSeek balance response");
764
806
  const infos = Array.isArray(body.balance_infos) ? body.balance_infos : [];
@@ -928,7 +970,7 @@ export function normalizeLitellm(keyBody, budgetBody) {
928
970
  return { windows, extras, coverage: "complete" };
929
971
  }
930
972
  function detectConfigured(ctx) {
931
- try { const ids = new Set(ctx.llm.listProviders().map(p => p.id)); return Object.fromEntries(IDS.map(id => [id, ids.has(id)])); }
973
+ try { const ids = new Set(ctx.llm.listProviders().map(p => p.id)); return Object.fromEntries(IDS.map(id => [id, ids.has(id) || id === "deepseek-account" && ids.has("deepseek-official")])); }
932
974
  catch { return undefined; }
933
975
  }
934
976
  const codec = (type, parse) => ({ mode: "strict", typeSymbol: `dsh-subusage#${type}`, schema: { parse }, create: () => ({ parse }) });
@@ -977,6 +1019,8 @@ export class SubUsageService extends TypertRemoteService {
977
1019
  // identity 解析(凭据服务/登录文件读取)与 HTTP 同量级的超时,默认 10s(对齐 fetchJson 的
978
1020
  // AbortSignal.timeout);经 options 注入以便测试用极短值,不必真等 10 秒。
979
1021
  this.identityTimeoutMs = options.identityTimeoutMs ?? 10000;
1022
+ this.accountBalanceTimeoutMs = options.accountBalanceTimeoutMs ?? 10000;
1023
+ this.deepseekAccount = options.resolveDeepseekAccount ?? (() => ctx.get?.("deepseekAccount"));
980
1024
  this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
981
1025
  this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
982
1026
  // 火山方舟的 AK/SK 不是路由的推理 Key,按固定变量名各自解析,两端都命中才算配对。
@@ -997,7 +1041,7 @@ export class SubUsageService extends TypertRemoteService {
997
1041
  // 过期时借 dsh-grok-kit 的官方会话链刷新同一登录文件;解析不到兄弟插件时返回 undefined 并降级。
998
1042
  this.resolveGrokKit = options.resolveGrokKit ?? loadGrokKit;
999
1043
  // Codex 只读共享登录文件($CODEX_HOME/auth.json 或 ~/.codex/auth.json)取 OAuth access token。
1000
- this.codexAuth = options.resolveCodexAuth ?? (async () => codexAuthFileCredentials(this.io, { CODEX_HOME: (await this.environmentByName("CODEX_HOME"))?.value }));
1044
+ this.codexAuth = options.resolveCodexAuth ?? (async () => (await codexSubscriptionCredentials(ctx.get?.("credentials"))) ?? codexAuthFileCredentials(this.io, { CODEX_HOME: (await this.environmentByName("CODEX_HOME"))?.value }));
1001
1045
  this.cache = new Map(); this.inflight = new Map(); this.generations = new Map(); this.fingerprints = new Map(); this.disposed = false; this.controller = new AbortController();
1002
1046
  this.login = new MimoLogin({
1003
1047
  error: failure,
@@ -1030,12 +1074,17 @@ export class SubUsageService extends TypertRemoteService {
1030
1074
  return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.userId ? { userId: cred.userId } : {}), ...(cred.expiresAt ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
1031
1075
  }
1032
1076
  if (id === "openai-codex") {
1033
- // 同上:只读 Codex CLI 的 OAuth 登录文件,token 由 Codex CLI 轮换,第三端刷新会互相顶掉。
1077
+ // 优先只读 Subscription 活动 OAuth,再回落 CLI;续约由凭据所属插件负责。
1034
1078
  const cred = await this.codexAuth();
1035
- return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}) } : { key: undefined, source: "none" };
1079
+ return cred?.key ? { key: cred.key, source: cred.source ?? "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}), ...(Number.isFinite(cred.expiresAt) ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
1036
1080
  }
1037
1081
  if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
1038
- if (id === "commandcode" && account && account !== "default") {
1082
+ if (id === "deepseek-account") {
1083
+ const service = this.deepseekAccount();
1084
+ const ready = typeof service?.getState === "function" && typeof service?.getBalance === "function" && (await service.getState())?.status === "credential-stored";
1085
+ return { key: undefined, source: ready ? "account-service" : "none" };
1086
+ }
1087
+ if (id === "commandcode" && account && account !== "default") {
1039
1088
  // 额外账户:id 即凭据引用名(apiKeyEnv)。引用名不合法(不可能来自提供方
1040
1089
  // 插件的 slots,但 client 输入不可信)按未配置处理,不让 credentialRef 抛错。
1041
1090
  if (!isCredentialRefName(account)) return { key: undefined, source: "none" };
@@ -1175,6 +1224,13 @@ export class SubUsageService extends TypertRemoteService {
1175
1224
  if (id === "minimax" || id === "minimax-cn") return normalizeMinimax(await get(PROVIDERS[id].usageUrl, headers));
1176
1225
  if (id === "siliconflow") return normalizeSiliconFlow(await get("https://api.siliconflow.cn/v1/user/info", headers));
1177
1226
  if (id === "deepseek") return normalizeDeepseekBalance(await get("https://api.deepseek.com/user/balance", headers));
1227
+ if (id === "deepseek-account") {
1228
+ const service = this.deepseekAccount();
1229
+ if (typeof service?.getBalance !== "function") throw failure("subusage/response", "本机 DeepSeek 账号余额服务不可用", true);
1230
+ const body = await withTimeout(service.getBalance({ version: VERSION, locale: "zh-CN", timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 }), this.accountBalanceTimeoutMs, () => failure("subusage/network", "本机 DeepSeek 账号余额读取超时", true));
1231
+ if (body === null) throw failure("subusage/auth", "本机 DeepSeek 账号未登录或登录已失效,请在 DSH 账号与余额页面确认");
1232
+ return normalizeDeepseekAccountBalance(body);
1233
+ }
1178
1234
  if (id === "openrouter") {
1179
1235
  const keyBody = await get("https://openrouter.ai/api/v1/key", headers);
1180
1236
  // 余额端点对普通推理 key 会 403;这一次失败不影响 key 限额与用量的展示。
@@ -1195,7 +1251,12 @@ export class SubUsageService extends TypertRemoteService {
1195
1251
  get("https://zenmux.ai/api/v1/management/subscription/detail", headers).catch(() => undefined),
1196
1252
  get("https://zenmux.ai/api/v1/management/payg/balance", headers).catch(() => undefined)
1197
1253
  ]);
1198
- return normalizeZenmux(subscription, balance);
1254
+ const data = normalizeZenmux(subscription, balance);
1255
+ if (subscription === undefined) {
1256
+ data.coverage = "partial";
1257
+ data.extras.push({ kind: "note", value: "余额已读取;套餐状态暂不可确认" });
1258
+ }
1259
+ return data;
1199
1260
  }
1200
1261
  if (id === "litellm") {
1201
1262
  // 地址是用户自己的 proxy;缺地址算「凭据不完整」,与缺 Key 一样给出可行动的指引。
@@ -1258,7 +1319,7 @@ export class SubUsageService extends TypertRemoteService {
1258
1319
  const base = "https://platform.xiaomimimo.com/api/v1";
1259
1320
  const h = { cookie: identity.key, origin: "https://platform.xiaomimimo.com", referer: "https://platform.xiaomimimo.com/#/console/balance", "x-timezone": "UTC+08:00" };
1260
1321
  let usageError;
1261
- const [bal, detail, usage] = await Promise.all([get(`${base}/balance`, h), get(`${base}/tokenPlan/detail`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/usage`, h).catch(e => { if (e.code === "subusage/auth") throw e; usageError = e; return undefined; })]);
1322
+ const [bal, detail, usage] = await Promise.all([get(`${base}/balance`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/detail`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/usage`, h).catch(e => { if (e.code === "subusage/auth") throw e; usageError = e; return undefined; })]);
1262
1323
  // 普通刷新与登录验证都经过这里:服务端字段不可信,缓存前去除 Cookie 回显。
1263
1324
  const data = redactMimo(normalizeMimo(bal, detail, usage), identity.key);
1264
1325
  if (usageError) data.usageError = usageError;
@@ -1383,7 +1444,7 @@ export class SubUsageService extends TypertRemoteService {
1383
1444
  if (startedGeneration !== (this.generations.get(cacheKey) ?? 0)) return this.provider(id, this.load(), force, account);
1384
1445
  if (!providerEnabled(settings, id)) {
1385
1446
  this.cache.delete(cacheKey); this.inflight.delete(cacheKey);
1386
- return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
1447
+ return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key || identity.source === "account-service", windows: [], extras: [] };
1387
1448
  }
1388
1449
  // fingerprint 计入账户选择:切换账户后同 id 缓存/inflight 不串账(不同账户 key 必不同);
1389
1450
  // 空串与 "default" 同为默认账户,归一后共享缓存。
@@ -1393,7 +1454,8 @@ export class SubUsageService extends TypertRemoteService {
1393
1454
  const generation = this.generations.get(cacheKey) ?? 0;
1394
1455
  const cached = this.cache.get(cacheKey);
1395
1456
  if (cached && cached.fingerprint !== fingerprint) this.cache.delete(cacheKey);
1396
- const previous = cached?.fingerprint === fingerprint ? cached : undefined;
1457
+ // 账号服务不提供会话指纹,也不读取其 token:每次查询复用其实时余额链,不跨账号复用本插件缓存。
1458
+ const previous = identity.source !== "account-service" && cached?.fingerprint === fingerprint ? cached : undefined;
1397
1459
  const active = this.inflight.get(cacheKey);
1398
1460
  if (active?.fingerprint === fingerprint && active.generation === generation) return active.promise;
1399
1461
  const now = this.now();
@@ -1401,7 +1463,7 @@ export class SubUsageService extends TypertRemoteService {
1401
1463
  if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
1402
1464
  const promise = (async () => {
1403
1465
  const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
1404
- if (!identity.key) entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
1466
+ if (!identity.key && identity.source !== "account-service") entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "deepseek-account" ? "未检测到本机 DeepSeek 账号登录:请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key" : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:在 Codex Subscription 设置页独立登录,或运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
1405
1467
  else try {
1406
1468
  const data = await this.fetchProvider(id, identity, settings);
1407
1469
  const usageError = data.usageError; delete data.usageError;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-subusage",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "displayName": "订阅用量显示",
5
- "description": "显示 DeepSeek 官方余额、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
5
+ "description": "显示独立 DeepSeek API Key 与 DSH 本机账号余额(账号入口默认开启)、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
6
6
  "keywords": [
7
7
  "dsh",
8
8
  "dsh-plugin",