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 +80 -71
- package/lib/client.js +94 -29
- package/lib/index.js +84 -22
- package/package.json +2 -2
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
|
-
|
|
7
|
+
在 DeepSeek Harness 的模型选择器旁看余量;点开药丸,查看额度窗口、重置时间与余额。
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
|
+
[](#license--security--许可与安全)
|
|
11
|
+
[](#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
|
-
|
|
31
|
+
1. **启用插件**:按部署方式重载,让 Host 与 Client 一起生效;只刷新页面不保证 Host 已升级。
|
|
32
|
+
2. **配置需要的提供商**:打开 **设置 → 订阅用量 → 提供商管理**。填 API Key,或使用对应提供方插件的登录态;MiMo 可“登录并自动导入”。[不知道该填什么?](docs/credentials.md)
|
|
33
|
+
3. **回到对话**:选择该提供商的模型,输入区出现用量药丸。点开看明细;读取失败会说明原因,不把未知当充足。
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
**已经登录 DSH 的 DeepSeek 账号?** 默认开启的「DeepSeek 账号」直接读取宿主余额,不需要复制 token。独立 API Key 使用另一个「DeepSeek API」入口,两份余额不相加、不互相替代。[两种入口的区别](docs/providers.md#deepseek-api-与-deepseek-账号)
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
<summary>深色主题</summary>
|
|
37
|
+
## 功能预览
|
|
36
38
|
|
|
37
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
无套餐窗口且数据完整时,药丸直接显示金额。人民币、美元与 credits 各按自己的规则展示;**余额不等于订阅额度,也不能盖过已耗尽的套餐**。DeepSeek API 与本机账号有各自的开关、凭据来源与余额。
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
### 管账户:需要时展开,不让长列表占满界面
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Command Code 可在弹层和设置页切换固定账户,或查看自动轮换池的健康度。逐账户详情默认折叠,受限周期明确标注。**自动轮换模式的用量仍是默认账户,不跟随实际服务中的账户。**
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
## 支持什么
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
**28 条数据入口,新安装默认开启 11 条。** 按使用场景,主要分为:
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
+
[查看完整支持清单](#支持的数据--supported-data) · [查看哪些经过真实账号验证](#support-status--实测覆盖)
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
本插件**只显示用量,不注册模型路由**。Codex 默认关闭,避免与提供方自带药丸重复;需要统一展示时可手动开启。私有额度接口可能变化,数字仅供参考,不是计费依据。
|
|
68
70
|
|
|
69
|
-
##
|
|
71
|
+
## 也可以直接问 Agent
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
dsh plugin --profile <profile> add dsh-subusage
|
|
73
|
-
```
|
|
73
|
+
“看一下 Kimi 和 Codex 还剩多少额度。”
|
|
74
74
|
|
|
75
|
-
|
|
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
|
-
|
|
77
|
+
## 使用前,先知道这三点
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# 或在 设置 → 订阅用量 → DeepSeek →「连接与凭据」里手动填写
|
|
86
|
-
```
|
|
79
|
+
- **不猜数据**:查询失败、缓存与部分覆盖会明确标注;接口读取成功不保证仍可续用。
|
|
80
|
+
- **不把凭据放进截图或结果**:API Key、Cookie 与 OAuth token 不回填界面;但本插件手动配置仍可能明文落盘,**它不是加密凭据库**。
|
|
81
|
+
- **验证有范围**:历史真实账号记录覆盖 7 个入口,其余主要依靠桩测试;新图是虚构数据离线渲染,不能作为真实账号在线验收证明。[实测范围与风险](#support-status--实测覆盖)
|
|
87
82
|
|
|
88
|
-
|
|
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
|
-
|
|
90
|
+
</details>
|
|
93
91
|
|
|
94
92
|
---
|
|
95
93
|
|
|
96
94
|
# 技术文档
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
安装、配置与排障参考。日常使用先看上面的快速开始,需要定位具体接口或凭据来源时再展开下文。
|
|
99
97
|
|
|
100
98
|
## 支持的数据 / Supported data
|
|
101
99
|
|
|
102
|
-
共 **
|
|
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
|
|
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
|
|
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)(
|
|
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
|
-
- **默认开关策略**:新安装默认开启
|
|
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`
|
|
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.
|
|
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
|

|
|
458
458
|
|
|
459
|
-
|
|
459
|
+
<details>
|
|
460
|
+
<summary>查看已发布基础药丸的 15 种状态(虚构数据)</summary>
|
|
461
|
+
|
|
462
|
+

|
|
463
|
+
|
|
464
|
+

|
|
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}
|
|
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 (!
|
|
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
|
|
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
|
-
?
|
|
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: "
|
|
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("
|
|
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("
|
|
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") : !
|
|
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
|
|
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.
|
|
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
|
|
522
|
-
|
|
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
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"displayName": "订阅用量显示",
|
|
5
|
-
"description": "
|
|
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",
|