dsh-subusage 0.12.0 → 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,124 +1,99 @@
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 把已支持的 28 个提供商数据入口的额度与余额装进 DeepSeek Harness 的输入框旁边——不切控制台、不猜百分比、不等报错。
7
+ 在 DeepSeek Harness 的模型选择器旁看余量;点开药丸,查看额度窗口、重置时间与余额。
9
8
 
10
- [![npm version](https://img.shields.io/badge/npm-0.12.0-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--兼容性)
13
12
 
14
- **安装只需一条命令** → [快速开始](#快速开始)
15
- </div>
16
-
17
- ---
13
+ [快速开始](#快速开始) · [功能预览](#功能预览) · [支持清单](#支持的数据--supported-data) · [凭据指引](docs/credentials.md)
18
14
 
19
- ## 为什么需要它
20
-
21
- | 没有它 | 有了它 |
22
- |---|---|
23
- | 额度散落在五六家控制台,查一次余额要开五个标签页 | 药丸常驻输入框旁:选中哪家模型就看哪家,多窗口自动取最差一窗 |
24
- | Agent 跑到一半撞上 429,才知道额度用尽 | 弹层先给结论再给依据:「即将用尽 · 最紧张:每周 · 剩 5% · 3 天后重置」 |
25
- | Cookie、OAuth 悄悄过期,报错才发现 | MiMo Cookie 提前 2 小时变黄、30 分钟变橙、过期变红;认证失效直说原因与修法 |
26
- | 每家插件各管各的凭据,格式五花八门 | 一个设置页集中管理 28 条路由的开关与凭据,缺什么补什么 |
15
+ <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/title.png" width="1200" alt="dsh-subusage:保留原标题图的深蓝构图,右侧展示真实 Codex 与 OpenCode Go 深浅主题用量弹层;虚构数据离线预览">
27
16
 
28
- ## 状态,一眼分清
17
+ <sub>真实组件 · 虚构数据 · 真实组件离线预览,不是运行中的 DSH 截图</sub>
29
18
 
30
- 药丸不写字也能传达状态:每个额度窗口一根彩条(绿 / 黄 / 橙 / 红,满格 = 100% 剩余),需要留意时才追加一个词。下面是已发布基础状态的离线预览(虚构用量数据);0.11.2 新增的续用/预算/能力范围状态见下节:
31
-
32
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-light.png" width="100%" alt="药丸 15 种状态(浅色,离线预览)">
33
-
34
- <details>
35
- <summary>深色主题</summary>
19
+ </div>
36
20
 
37
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-dark.png" width="100%" alt="药丸 15 种状态(深色,离线预览)">
21
+ > **0.13.0**:余额显示更清晰,设置按需刷新,兼容 Codex Subscription 独立 OAuth。发布与本机升级分开;本页图片是虚构数据离线预览,不是实机验收证明。[查看更新记录](docs/changelog.md)。
38
22
 
39
- </details>
23
+ ## 快速开始
40
24
 
41
- ## 三块能力,一个设置页
25
+ ```console
26
+ dsh plugin --profile web add dsh-subusage
27
+ ```
42
28
 
43
- | 能力 | 解决什么问题 | 入口 |
44
- |---|---|---|
45
- | 余量药丸 | 不用切到各家控制台,选中模型就知道这家还剩多少 | 模型选择器左侧,槽位 `conversation.input.right` |
46
- | 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
47
- | 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
29
+ `web` 是示例 Profile,请替换为实际目标。**Desktop 使用目标桌面壳提供的 DSH Desktop CLI**,不要从独立公共 CLI 管理保留的 `desktop` Profile。
48
30
 
49
- 套餐药丸以图形为主:每个额度窗口一根彩色余量条(绿 / 黄 / 橙 / 红,满 = 100% 剩余),正常时不写字;需要留意时追加一个词,如 `周 偏少`、`5时 已用尽`、`需配置`、`拉取失败`。一般成功结果按提供商缓存 **60 秒**,手动刷新可跳过普通 TTL(仍遵守上游限流退避与 `Retry-After`);DeepSeek 账号入口每次调用宿主余额链,不复用本插件的旧余额缓存,避免切换账号后显示另一份钱包。
31
+ 1. **启用插件**:按部署方式重载,让 Host 与 Client 一起生效;只刷新页面不保证 Host 已升级。
32
+ 2. **配置需要的提供商**:打开 **设置 → 订阅用量 → 提供商管理**。填 API Key,或使用对应提供方插件的登录态;MiMo 可“登录并自动导入”。[不知道该填什么?](docs/credentials.md)
33
+ 3. **回到对话**:选择该提供商的模型,输入区出现用量药丸。点开看明细;读取失败会说明原因,不把未知当充足。
50
34
 
51
- 本插件**不注册模型路由**,药丸按 provider id 匹配;因此使用哪个插件提供该模型路由不影响匹配,id 对不上时才不显示。
35
+ **已经登录 DSH 的 DeepSeek 账号?** 默认开启的「DeepSeek 账号」直接读取宿主余额,不需要复制 token。独立 API Key 使用另一个「DeepSeek API」入口,两份余额不相加、不互相替代。[两种入口的区别](docs/providers.md#deepseek-api-与-deepseek-账号)
52
36
 
53
- ## 28 个提供商数据入口,默认开启 11 个
37
+ ## 功能预览
54
38
 
55
- DeepSeek API、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——订阅窗口、余额、周期额度池都支持。**新安装默认开启 11 条**,其余在设置页一键开启;完整清单、额度类型与鉴权方式见[下方技术文档](#支持的数据--supported-data)。
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>
56
43
 
57
- ### 0.12.0:双 DeepSeek 入口、余额药丸与折叠详情
44
+ ### 看额度:最紧张的窗口,先提醒你
58
45
 
59
- 本次发布包括以下改进;发布包不自动更新你的本机运行环境,也不代替真实账号验收:
46
+ 一个窗口一根余量条。正常时保持简洁;周窗偏少、额度耗尽、读取失败时,药丸明确提示。点开后再看百分比、适用范围与重置倒计时,不需要先切到控制台。
60
47
 
61
- - **Command Code 轮换池**逐账户行标明具体受限周期,详情默认折叠为「点击查看详情」,展开后限高滚动。
62
- - **余额药丸分级**:药丸直接显示金额与单位,不再一律中性灰。人民币 < 1 元红、1–5 元黄、≥ 5 元灰;美元 < 0.2 红、0.2–1 黄、≥ 1 灰;`credits` 及其它无统一价值单位只在已解析的非正余额时标红,正余额保持灰色而不按绝对量虚构价值。这些是**产品预警阈值,不是汇率换算,也不是「还能跑多少次」的保证**。累计花费不套用余额阈值。
63
- - **新增 `deepseek-account`(DeepSeek 账号)**:默认开启,复用 DSH 本机 `deepseekAccount` 服务的登录态读取充值余额;不读取、导出或保存 OAuth token,也不使用独立 API Key。与 `deepseek`(DeepSeek API)的开关、缓存和余额完全独立;`deepseek-official` 这个名字指向账号入口。纯余额型条目(两个 DeepSeek 入口与 SiliconFlow、Novita、Hyperbolic、DeepInfra、Vercel AI Gateway)不要求本机装有对应模型路由。
64
- - **MiMo 无订阅不再画成 0%**:只有「用量组为空 / 零容量」配合可读的套餐详情才判未订阅;零容量不生成 0% 用量窗口。套餐读取失败仍标部分数据,余额读取失败仍保留已成功的用量。
65
- - **ZenMux 订阅端点失败**保留余额并标部分数据,明确写「套餐状态暂不可确认」,不宣称未订阅。
48
+ ### 看余额:金额直接显示,单位不混淆
66
49
 
67
- 35 个测试文件、清单与语法检查、深浅主题各 11 个余额场景及 40 账户折叠验收已通过。离线预览使用虚构数据,不代表新账号入口或无订阅响应已经真实在线验收——详见[开发说明](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md)。
50
+ 无套餐窗口且数据完整时,药丸直接显示金额。人民币、美元与 credits 各按自己的规则展示;**余额不等于订阅额度,也不能盖过已耗尽的套餐**。DeepSeek API 与本机账号有各自的开关、凭据来源与余额。
68
51
 
69
- <details>
70
- <summary>余额药丸与套餐优先:0.12.0 深色离线预览</summary>
52
+ ### 管账户:需要时展开,不让长列表占满界面
71
53
 
72
- <img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/balance-pills-dark.png" width="586" alt="余额药丸分单位预警、MiMo 无套餐与套餐优先,真实组件离线渲染,使用虚构金额">
54
+ Command Code 可在弹层和设置页切换固定账户,或查看自动轮换池的健康度。逐账户详情默认折叠,受限周期明确标注。**自动轮换模式的用量仍是默认账户,不跟随实际服务中的账户。**
73
55
 
74
- </details>
56
+ ## 支持什么
75
57
 
76
- ## 额度与续用:0.11.2 的关键修正
58
+ **28 条数据入口,新安装默认开启 11 条。** 按使用场景,主要分为:
77
59
 
78
- - **Codex Pro / ProLite / ProMax**:合法单周窗口正常展示,不再因没有 5 小时窗而黄警;多工作区查询携带账户身份,企业支出限制与 Credits 单独保留。
79
- - **ZenMux / Novita / LiteLLM**:补齐 ZenMux 周窗;Novita 接受官方金额字符串;LiteLLM 的 Key、用户、团队预算分别展示,任何一层都不会被更宽松的预算覆盖。
80
- - **包含额度耗尽不等于服务停摆**:Command Code 有已购余额时显示“按量续用”;NanoGPT 使用接口路由建议,未知余额/策略明确待确认。其他厂商未核验的续用条件不作保证。
81
- - **轮换与能力范围**:自动轮换池尚有健康账户,不因默认账户限额报全池耗尽;AFP 文本周限制不连坐日能力池,共享月额度仍影响各能力。部分能力耗尽只提示范围,不冒充整个套餐不可用。
82
- - **数据完整性**:合法单窗、无上限和纯余额成功不是“不完整”;真实端点读取失败仍可标部分覆盖。金额或窗口未知时不编造百分比。
83
-
84
- 以上均已通过离线回归;不代表真实企业席位、新旧私有接口或付费资格已经在线验收。详细契约与仍待核验范围见[开发说明](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md#额度语义修正0112)。
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 |
85
66
 
86
- [快速开始](#快速开始) · [支持清单](#支持的数据--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)
67
+ [查看完整支持清单](#支持的数据--supported-data) · [查看哪些经过真实账号验证](#support-status--实测覆盖)
87
68
 
88
- ## 快速开始
69
+ 本插件**只显示用量,不注册模型路由**。Codex 默认关闭,避免与提供方自带药丸重复;需要统一展示时可手动开启。私有额度接口可能变化,数字仅供参考,不是计费依据。
89
70
 
90
- ```console
91
- dsh plugin --profile web add dsh-subusage
92
- ```
71
+ ## 也可以直接问 Agent
93
72
 
94
- `web` 仅为示例 Profile,替换为实际目标;Desktop 应使用目标桌面壳提供的 DSH Desktop CLI,不从普通公共 CLI 管理 `desktop`。
73
+ “看一下 Kimi 和 Codex 还剩多少额度。”
95
74
 
96
- 1. 安装并启用 bundle,按部署方式重载或重启(只刷新页面不保证 Host 升级)。
97
- 2. 打开 **设置 → 订阅用量 → 提供商管理**,开启需要的提供商并填凭据——每类凭据的官网入口与填入位置见[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md);MiMo 用「登录并自动导入」。
98
- 3. 回到会话,选中该提供商的模型:输入区左侧出现药丸,点开看窗口明细。
99
- 4. 排障:点「立即刷新」→ 看设置页顶部 `X/Y 家数据获取成功`(可点击,直接跳到第一家没读成功的)。
75
+ 模型可调用只读工具 `subusage_quota`,读取与界面同源的额度与余额;可按提供商筛选,Command Code 可分别返回各账户状态。不会通过该工具修改设置。[工具与 Host 服务契约](#额度查询-api)
100
76
 
101
- **DSH 已登录账号**:默认开启的「DeepSeek 账号」直接读取宿主账号余额,不需要复制 OAuth token 或填写 API Key;用 `subusage_quota(providers: ["deepseek-account"])` 查询。
77
+ ## 使用前,先知道这三点
102
78
 
103
- **独立 API Key**:使用「DeepSeek API」(`deepseek`),与账号入口分别管理,不接 OAuth:
79
+ - **不猜数据**:查询失败、缓存与部分覆盖会明确标注;接口读取成功不保证仍可续用。
80
+ - **不把凭据放进截图或结果**:API Key、Cookie 与 OAuth token 不回填界面;但本插件手动配置仍可能明文落盘,**它不是加密凭据库**。
81
+ - **验证有范围**:历史真实账号记录覆盖 7 个入口,其余主要依靠桩测试;新图是虚构数据离线渲染,不能作为真实账号在线验收证明。[实测范围与风险](#support-status--实测覆盖)
104
82
 
105
- ```powershell
106
- # 任选一种凭据来源
107
- $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需按部署方式重启
108
- # 或在 设置 → 订阅用量 → DeepSeek API →「连接与凭据」里手动填写
109
- ```
83
+ <details>
84
+ <summary><strong>版本与验证范围:本页截图展示了什么?</strong></summary>
110
85
 
111
- 启动环境变更需要按部署方式重载;手动保存 Key 后可直接刷新。选中对应模型时药丸显示余额,也可让 Agent 调用 `subusage_quota(providers: ["deepseek"])` 查询独立 API 余额。两种余额读取均不要求安装模型路由;不会把两份余额相加或互相替代。
86
+ 标题图和三功能特写由当前 `lib/client.js` 的真实组件生成,未制作平行 UI。画面使用虚构金额与账户;绿色余额图标、最多两位小数与独立 Codex Subscription OAuth 兼容纳入 **0.13.0**。OAuth 续约仍由凭据所属插件负责;未替代真实账号在线验收。
112
87
 
113
- ## 先说清楚:哪些是实测过的
88
+ 0.12.0 已发布双 DeepSeek 入口、分单位余额预警与 Command Code 详情折叠;该版本正常余额为灰色。历史截图与每轮验证范围见[更新记录](docs/changelog.md)和[开发说明](docs/development.md)。
114
89
 
115
- **7 家在真实账号上跑通**:火山方舟 Agent Plan、火山方舟 Coding Plan、Kimi Coding、小米 MiMo、Command Code、SuperGrok、Codex (ChatGPT)。**其余只经过桩测试**——响应形态取自公开文档、第三方实现或抓包,没有在真实账号上跑通,可能随接口变化而失效;失效时在设置页关掉那一家即可,其余不受影响。验证范围与主要风险见[实测覆盖](#support-status--实测覆盖)。额度数字仅供参考,不要当计费依据。
90
+ </details>
116
91
 
117
92
  ---
118
93
 
119
94
  # 技术文档
120
95
 
121
- > 安装、配置、排障的完整参考。快速上手看上面就够了。
96
+ 安装、配置与排障参考。日常使用先看上面的快速开始,需要定位具体接口或凭据来源时再展开下文。
122
97
 
123
98
  ## 支持的数据 / Supported data
124
99
 
@@ -134,7 +109,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
134
109
  | OpenCode Go | `opencode-go` | 订阅窗口 | Bearer | 开 |
135
110
  | Command Code | `commandcode` | 5 小时 / 周 / 月额度池 | 提供方插件的凭据链 | 开 |
136
111
  | SuperGrok | `xai-oauth` | 周期池 | 共享 Grok CLI 登录文件,过期经 grok-kit 续约 | 开 |
137
- | Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Codex CLI 的 OAuth 登录文件 | 关 |
112
+ | Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Subscription 独立 OAuth(0.13.0 起)或 Codex CLI 登录文件 | 关 |
138
113
  | MiniMax(中国) | `minimax-cn` | 短周期 / 周 | Bearer | 开 |
139
114
  | ARK Agent Plan (arkcli) | `arkcli-agent-plan` | 订阅窗口(AFP) | IAM AK/SK 签名 | 开 |
140
115
  | ARK Coding Plan (arkcli) | `arkcli-coding-plan` | 订阅窗口 | IAM AK/SK 签名 | 开 |
@@ -252,7 +227,7 @@ dsh plugin --profile <profile> remove dsh-subusage
252
227
  | --- | --- | --- |
253
228
  | Command Code | [`@mars-sea/dsh-commandcode-provider`](https://www.npmjs.com/package/@mars-sea/dsh-commandcode-provider) | 在该插件的设置页登录,或运行 `cmd login`(兜底读 `~/.commandcode/auth.json`) |
254
229
  | SuperGrok | [`dsh-grok-kit`](https://github.com/KouzakiUmi/dsh-grok-kit) | 运行 `grok login`,登录文件 `~/.grok/auth.json` |
255
- | 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 |
256
231
 
257
232
  > **Codex 默认关闭**:它的提供方插件本身就带一个用量药丸,两个并排只是重复信息。想要本插件这一份(比如想把 Codex 和别家放在一起看)可以在**提供商管理**里手动开启。
258
233
  >
@@ -288,7 +263,7 @@ dsh plugin --profile <profile> remove dsh-subusage
288
263
  | MiniMax(中国) | `MINIMAX_CN_API_KEY` | `minimax-cn` 路由;中国站订阅 Key |
289
264
  | Command Code | `COMMANDCODE_API_KEY` | 凭据由提供方插件管理,本页只读继承;兜底读取 `~/.commandcode/auth.json`(`cmd login`) |
290
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`)取不到订阅周池 |
291
- | 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,续约由所属插件负责 |
292
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」 |
293
268
  | SiliconFlow | `SILICONFLOW_API_KEY` | 余额型:`GET /v1/user/info` 返回账户余额,**与推理是同一把 Key** |
294
269
  | OpenRouter | `OPENROUTER_API_KEY` | key 限额与用量用推理 key 即可;**账户余额需要 management / provisioning key**,普通 key 会被 403 拒——此时静默降级为只显示限额,不判失败 |
@@ -467,7 +442,7 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
467
442
 
468
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)。
469
444
 
470
- > 打包说明:`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 页面无需把图片打进包也能显示。改动白名单属于发布决策,需单独处理。
471
446
 
472
447
  ### 界面预览
473
448
 
@@ -481,7 +456,16 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
481
456
 
482
457
  ![浅色 NanoGPT 用量弹层(离线测试预览)](https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/usage-popover-light.png)
483
458
 
484
- 药丸 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 的功能特写见[功能预览](#功能预览)。
485
469
 
486
470
  截图声明见根目录 [`screenshots.json`](screenshots.json)。上述四张保留 0.10.11 的基础界面预览;下方三张为 0.11.2 新增语义状态的真实组件离线预览(虚构用量、桩网络、无凭据),底部水印明确标注「非运行中的 DSH 截图」。真实 DSH Loader、实际账号在线接口与多账户映射仍未验收,截图不代表已通过这些检查。
487
471
 
package/lib/client.js CHANGED
@@ -417,7 +417,8 @@ window.__ModuleLoader__.load({
417
417
  function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
418
418
  const BALANCE_ONLY_IDS = new Set(["deepseek", "deepseek-account", "siliconflow", "novita", "hyperbolic", "deepinfra", "vercel-ai-gateway"]);
419
419
  function providerCanQuery(result, id) {
420
- return BALANCE_ONLY_IDS.has(id) || !!PROVIDER_META[id]?.volc || !!result?.settings?.hasKeys?.[id] || result?.settings?.keyModes?.[id] === "manual" || providerRoutable(result, id);
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);
421
422
  }
422
423
  function providerVisible(settings, id, entry, result) {
423
424
  if (!providerEnabled(settings, id)) return false;
@@ -484,9 +485,19 @@ window.__ModuleLoader__.load({
484
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");
485
486
  }
486
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
+ }
487
498
  function balanceText(entry, t) {
488
499
  const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
489
- return row ? `${extraLabel(row, t)} ${row.value}` : null;
500
+ return row ? `${extraLabel(row, t)} ${row.kind === "balance" ? formatBalanceValue(row.value) : row.value}` : null;
490
501
  }
491
502
  /** 余额只按自己的单位预警,不做汇率折算,也不把累计消费当余额。 */
492
503
  function balanceVisual(entry, t) {
@@ -500,11 +511,11 @@ window.__ModuleLoader__.load({
500
511
  const amount = suffix ? Number(suffix[1]) : prefix ? Number(prefix[2]) : bare ? Number(bare[1]) : NaN;
501
512
  const unit = suffix ? suffix[2].toUpperCase() : prefix ? prefix[1] === "$" ? "USD" : "CNY" : null;
502
513
  const rawAmount = suffix?.[1] ?? prefix?.[2];
503
- const value = unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text;
514
+ const value = formatBalanceValue(unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text);
504
515
  // 产品预警阈值,不是汇率或能运行多少次的保证。credits 没有跨厂商统一价值。
505
516
  const limits = unit === "CNY" ? { red: 1, amber: 5 } : unit === "USD" ? { red: 0.2, amber: 1 } : null;
506
517
  const rank = Number.isFinite(amount) ? amount <= 0 || limits && amount < limits.red ? 2 : limits && amount < limits.amber ? 1 : 0 : 0;
507
- return { value, color: rank === 2 ? "#ef4444" : rank === 1 ? "#eab308" : "#94a3b8", tinted: rank > 0, label: rank > 0 ? t("balanceLow") : "" };
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") : "" };
508
519
  }
509
520
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
510
521
  function durationText(ms, t) {
@@ -782,7 +793,7 @@ window.__ModuleLoader__.load({
782
793
  /** 每次 apply 一个 store;partial response 只合并请求项,所有槽共享 inflight 与活动计时。 */
783
794
  function createUsageStore(call) {
784
795
  let snapshot = { updatedAt: null, settings: null, configured: {}, entries: [] };
785
- 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;
786
797
  let loginGeneration = 0, loginJobId = null, loginPending = false;
787
798
  // Command Code 显示账户:弹层账户区选中的账户("" = 默认账户/顶层 Key)。
788
799
  // 所有 commandcode 读取(readAll/refresh/定时刷新)都携带它,切换后用量同步跟随。
@@ -883,6 +894,15 @@ window.__ModuleLoader__.load({
883
894
  };
884
895
  return {
885
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
+ },
886
906
  getCommandCodeAccount: () => ccAccount, setCommandCodeAccount,
887
907
  startMimoLogin: async (request) => { const generation = ++loginGeneration, startedEpoch = epoch; loginJobId = null; return acceptLogin(await call("startMimoLogin", request), generation, startedEpoch); },
888
908
  getMimoLoginStatus: async () => { const generation = loginGeneration, startedEpoch = epoch, jobId = loginPending ? loginJobId : null; return acceptLogin(await call("getMimoLoginStatus"), generation, startedEpoch, jobId); },
@@ -1203,7 +1223,7 @@ window.__ModuleLoader__.load({
1203
1223
  if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
1204
1224
  const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
1205
1225
  const wallet = !windows.length && rawEntry.coverage === "complete" && rawEntry.availability === "paygo" ? balanceVisual(rawEntry, t) : null;
1206
- if (wallet && !expiryActionable) return { ...base, key: "paygo", color: wallet.color, icon: "", title: balanceText(rawEntry, t), sub: [title, wallet.label].filter(Boolean).join(" · "), short: wallet.value, tinted: wallet.tinted };
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 };
1207
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 };
1208
1228
  }
1209
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");
@@ -1237,7 +1257,7 @@ window.__ModuleLoader__.load({
1237
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 };
1238
1258
  // 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
1239
1259
  const balance = balanceText(entry, t), visual = balanceVisual(entry, t);
1240
- return { ...base, key: "balance", color: visual?.color ?? NEUTRAL, icon: visual ? "" : "ℹ", title: balance ?? t("noUsageData"), sub: visual?.label ?? "", short: visual?.value ?? balance ?? t("noUsageData"), tinted: visual?.tinted ?? false };
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 };
1241
1261
  }
1242
1262
  /** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
1243
1263
  function statusBanner(status) {
@@ -1409,7 +1429,7 @@ window.__ModuleLoader__.load({
1409
1429
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1410
1430
  // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
1411
1431
  // 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
1412
- const tip = `${label}: ${st.title}${st.icon === "" && st.sub ? " · " + st.sub : ""}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
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 : ""}`;
1413
1433
  // 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
1414
1434
  // 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
1415
1435
  const glyph = windows.length ? miniBars(windows, degraded) : st.icon === "" ? null : statusGlyph(st.color, st.icon, 16);
@@ -1425,7 +1445,7 @@ window.__ModuleLoader__.load({
1425
1445
  display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
1426
1446
  borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
1427
1447
  background: st.tinted ? alpha(st.color, ".10") : "transparent",
1428
- color: st.icon === "" && st.tinted ? `color-mix(in srgb, ${st.color} 65%, currentColor)` : "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
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"
1429
1449
  }
1430
1450
  },
1431
1451
  glyph,
@@ -1469,7 +1489,7 @@ window.__ModuleLoader__.load({
1469
1489
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1470
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" } },
1471
1491
  h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1472
- 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))))
1473
1493
  : null,
1474
1494
  providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1475
1495
  // 页脚:更新时间 + 刷新。
@@ -1565,14 +1585,14 @@ window.__ModuleLoader__.load({
1565
1585
  const text = (cn, en) => local ? cn : en;
1566
1586
  react.useEffect(() => {
1567
1587
  let alive = true;
1568
- 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)); });
1569
1589
  return () => { alive = false; };
1570
1590
  }, [usageStore]);
1571
1591
  react.useEffect(() => {
1572
1592
  let alive = true;
1573
1593
  const enabled = editorOpen && providerEnabled(result.settings, tab);
1574
1594
  const deactivate = enabled ? usageStore.activate(tab) : () => {};
1575
- 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)); });
1576
1596
  return () => { alive = false; deactivate(); };
1577
1597
  }, [usageStore, tab, editorOpen, providerEnabled(result.settings, tab)]);
1578
1598
  react.useEffect(() => {
@@ -1632,7 +1652,7 @@ window.__ModuleLoader__.load({
1632
1652
  };
1633
1653
  const refresh = async (all) => {
1634
1654
  setRefreshing(true);
1635
- 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); }
1636
1656
  catch (error) { if (aliveRef.current) setLoadError(localError(error.code, String(error.message || error), t)); }
1637
1657
  finally { if (aliveRef.current) setRefreshing(false); }
1638
1658
  };
@@ -1741,7 +1761,7 @@ window.__ModuleLoader__.load({
1741
1761
  const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
1742
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)")
1743
1763
  : text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)");
1744
- const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel, "account-service": text("本机 DeepSeek 账号余额", "Local DeepSeek account balance") };
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)") };
1745
1765
  const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
1746
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,
1747
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,
@@ -1762,7 +1782,7 @@ window.__ModuleLoader__.load({
1762
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,
1763
1783
  h("p", null, `${result.configured && result.configured[tab] ? t("detected") : t("notDetected")} · ${sources[source] || sources.none}`),
1764
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,
1765
- h("p", null, text("只读 Codex CLI 的 ChatGPT 订阅登录($CODEX_HOME/auth.json 或 ~/.codex/auth.json):本页不保存凭据、不刷新 token。请用 codex login 登录;API Key 模式(auth_mode 不是 chatgpt)取不到订阅额度。", "Reads the Codex CLI ChatGPT subscription login ($CODEX_HOME/auth.json or ~/.codex/auth.json) read-only: this page never saves credentials or refreshes tokens. Sign in with codex login; API-key mode (auth_mode not chatgpt) cannot read subscription quota."))) : PROVIDER_META[tab].volc ? h(react.Fragment, null,
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,
1766
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.")),
1767
1787
  // 用户通常已经登录着控制台,直接给入口比让他自己翻菜单快得多。
1768
1788
  h("p", { style: { fontSize: 12 } }, externalLink("https://console.volcengine.com/iam/keymanage", text("打开火山引擎「API 访问密钥」创建 AK/SK", "Open Volcengine “API access keys”")), " · ", credentialsGuide),
@@ -1892,7 +1912,7 @@ window.__ModuleLoader__.load({
1892
1912
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1893
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" } },
1894
1914
  h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1895
- 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))))
1896
1916
  : null,
1897
1917
  // 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
1898
1918
  // 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
@@ -1962,7 +1982,12 @@ window.__ModuleLoader__.load({
1962
1982
  /** 槽包装:先看当前模型的 provider,再分发到对应药丸。 */
1963
1983
  function SubusagePillEntry({ store, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
1964
1984
  const state = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1965
- 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);
1966
1991
  // 旧插件 @volcengine/ark-plan-api 的两个国内路由 id 不在本表里,但它们是**同一个 plan**:
1967
1992
  // 不管模型路由由哪个插件提供,都归并到 arkcli 路由再查——所以只会有**一个**面板,
1968
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.12.0";
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" },
@@ -267,6 +267,30 @@ function grokAuthFileCredentials(io) {
267
267
  // 与 Grok 一样只读:token 由 Codex CLI 自己轮换,第三端刷新会互相顶掉。
268
268
  const CODEX_AUTH_DIR = join(homedir(), ".codex");
269
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
+ }
270
294
  export function codexAuthFileCredentials(io, env) {
271
295
  const home = typeof env?.CODEX_HOME === "string" && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : CODEX_AUTH_DIR;
272
296
  try {
@@ -1017,7 +1041,7 @@ export class SubUsageService extends TypertRemoteService {
1017
1041
  // 过期时借 dsh-grok-kit 的官方会话链刷新同一登录文件;解析不到兄弟插件时返回 undefined 并降级。
1018
1042
  this.resolveGrokKit = options.resolveGrokKit ?? loadGrokKit;
1019
1043
  // Codex 只读共享登录文件($CODEX_HOME/auth.json 或 ~/.codex/auth.json)取 OAuth access token。
1020
- 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 }));
1021
1045
  this.cache = new Map(); this.inflight = new Map(); this.generations = new Map(); this.fingerprints = new Map(); this.disposed = false; this.controller = new AbortController();
1022
1046
  this.login = new MimoLogin({
1023
1047
  error: failure,
@@ -1050,9 +1074,9 @@ export class SubUsageService extends TypertRemoteService {
1050
1074
  return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.userId ? { userId: cred.userId } : {}), ...(cred.expiresAt ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
1051
1075
  }
1052
1076
  if (id === "openai-codex") {
1053
- // 同上:只读 Codex CLI 的 OAuth 登录文件,token 由 Codex CLI 轮换,第三端刷新会互相顶掉。
1077
+ // 优先只读 Subscription 活动 OAuth,再回落 CLI;续约由凭据所属插件负责。
1054
1078
  const cred = await this.codexAuth();
1055
- 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" };
1056
1080
  }
1057
1081
  if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
1058
1082
  if (id === "deepseek-account") {
@@ -1439,7 +1463,7 @@ export class SubUsageService extends TypertRemoteService {
1439
1463
  if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
1440
1464
  const promise = (async () => {
1441
1465
  const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
1442
- 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 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" };
1443
1467
  else try {
1444
1468
  const data = await this.fetchProvider(id, identity, settings);
1445
1469
  const usageError = data.usageError; delete data.usageError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subusage",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "displayName": "订阅用量显示",
5
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": [