dsh-subusage 0.11.2 → 0.12.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 +42 -17
- package/lib/client.js +60 -20
- package/lib/index.js +56 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
**选中的模型还剩多少额度,输入框旁边一眼可见。**
|
|
7
7
|
|
|
8
|
-
dsh-subusage 把已支持的
|
|
8
|
+
dsh-subusage 把已支持的 28 个提供商数据入口的额度与余额装进 DeepSeek Harness 的输入框旁边——不切控制台、不猜百分比、不等报错。
|
|
9
9
|
|
|
10
|
-
[](https://www.npmjs.com/package/dsh-subusage)
|
|
11
11
|
[](#license--security--许可与安全)
|
|
12
12
|
[](#compatibility--兼容性)
|
|
13
13
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| 额度散落在五六家控制台,查一次余额要开五个标签页 | 药丸常驻输入框旁:选中哪家模型就看哪家,多窗口自动取最差一窗 |
|
|
24
24
|
| Agent 跑到一半撞上 429,才知道额度用尽 | 弹层先给结论再给依据:「即将用尽 · 最紧张:每周 · 剩 5% · 3 天后重置」 |
|
|
25
25
|
| Cookie、OAuth 悄悄过期,报错才发现 | MiMo Cookie 提前 2 小时变黄、30 分钟变橙、过期变红;认证失效直说原因与修法 |
|
|
26
|
-
| 每家插件各管各的凭据,格式五花八门 | 一个设置页集中管理
|
|
26
|
+
| 每家插件各管各的凭据,格式五花八门 | 一个设置页集中管理 28 条路由的开关与凭据,缺什么补什么 |
|
|
27
27
|
|
|
28
28
|
## 状态,一眼分清
|
|
29
29
|
|
|
@@ -46,13 +46,32 @@
|
|
|
46
46
|
| 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
|
|
47
47
|
| 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
套餐药丸以图形为主:每个额度窗口一根彩色余量条(绿 / 黄 / 橙 / 红,满 = 100% 剩余),正常时不写字;需要留意时追加一个词,如 `周 偏少`、`5时 已用尽`、`需配置`、`拉取失败`。一般成功结果按提供商缓存 **60 秒**,手动刷新可跳过普通 TTL(仍遵守上游限流退避与 `Retry-After`);DeepSeek 账号入口每次调用宿主余额链,不复用本插件的旧余额缓存,避免切换账号后显示另一份钱包。
|
|
50
50
|
|
|
51
51
|
本插件**不注册模型路由**,药丸按 provider id 匹配;因此使用哪个插件提供该模型路由不影响匹配,id 对不上时才不显示。
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## 28 个提供商数据入口,默认开启 11 个
|
|
54
54
|
|
|
55
|
-
DeepSeek
|
|
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)。
|
|
56
|
+
|
|
57
|
+
### 0.12.0:双 DeepSeek 入口、余额药丸与折叠详情
|
|
58
|
+
|
|
59
|
+
本次发布包括以下改进;发布包不自动更新你的本机运行环境,也不代替真实账号验收:
|
|
60
|
+
|
|
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 订阅端点失败**保留余额并标部分数据,明确写「套餐状态暂不可确认」,不宣称未订阅。
|
|
66
|
+
|
|
67
|
+
35 个测试文件、清单与语法检查、深浅主题各 11 个余额场景及 40 账户折叠验收已通过。离线预览使用虚构数据,不代表新账号入口或无订阅响应已经真实在线验收——详见[开发说明](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md)。
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary>余额药丸与套餐优先:0.12.0 深色离线预览</summary>
|
|
71
|
+
|
|
72
|
+
<img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/balance-pills-dark.png" width="586" alt="余额药丸分单位预警、MiMo 无套餐与套餐优先,真实组件离线渲染,使用虚构金额">
|
|
73
|
+
|
|
74
|
+
</details>
|
|
56
75
|
|
|
57
76
|
## 额度与续用:0.11.2 的关键修正
|
|
58
77
|
|
|
@@ -69,23 +88,27 @@ DeepSeek、火山方舟(Agent / Coding Plan 个人版与企业版席位)、Z
|
|
|
69
88
|
## 快速开始
|
|
70
89
|
|
|
71
90
|
```console
|
|
72
|
-
dsh plugin --profile
|
|
91
|
+
dsh plugin --profile web add dsh-subusage
|
|
73
92
|
```
|
|
74
93
|
|
|
94
|
+
`web` 仅为示例 Profile,替换为实际目标;Desktop 应使用目标桌面壳提供的 DSH Desktop CLI,不从普通公共 CLI 管理 `desktop`。
|
|
95
|
+
|
|
75
96
|
1. 安装并启用 bundle,按部署方式重载或重启(只刷新页面不保证 Host 升级)。
|
|
76
97
|
2. 打开 **设置 → 订阅用量 → 提供商管理**,开启需要的提供商并填凭据——每类凭据的官网入口与填入位置见[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md);MiMo 用「登录并自动导入」。
|
|
77
98
|
3. 回到会话,选中该提供商的模型:输入区左侧出现药丸,点开看窗口明细。
|
|
78
99
|
4. 排障:点「立即刷新」→ 看设置页顶部 `X/Y 家数据获取成功`(可点击,直接跳到第一家没读成功的)。
|
|
79
100
|
|
|
80
|
-
|
|
101
|
+
**DSH 已登录账号**:默认开启的「DeepSeek 账号」直接读取宿主账号余额,不需要复制 OAuth token 或填写 API Key;用 `subusage_quota(providers: ["deepseek-account"])` 查询。
|
|
102
|
+
|
|
103
|
+
**独立 API Key**:使用「DeepSeek API」(`deepseek`),与账号入口分别管理,不接 OAuth:
|
|
81
104
|
|
|
82
105
|
```powershell
|
|
83
106
|
# 任选一种凭据来源
|
|
84
107
|
$env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需按部署方式重启
|
|
85
|
-
# 或在 设置 → 订阅用量 → DeepSeek →「连接与凭据」里手动填写
|
|
108
|
+
# 或在 设置 → 订阅用量 → DeepSeek API →「连接与凭据」里手动填写
|
|
86
109
|
```
|
|
87
110
|
|
|
88
|
-
|
|
111
|
+
启动环境变更需要按部署方式重载;手动保存 Key 后可直接刷新。选中对应模型时药丸显示余额,也可让 Agent 调用 `subusage_quota(providers: ["deepseek"])` 查询独立 API 余额。两种余额读取均不要求安装模型路由;不会把两份余额相加或互相替代。
|
|
89
112
|
|
|
90
113
|
## 先说清楚:哪些是实测过的
|
|
91
114
|
|
|
@@ -99,11 +122,12 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
99
122
|
|
|
100
123
|
## 支持的数据 / Supported data
|
|
101
124
|
|
|
102
|
-
共 **
|
|
125
|
+
共 **28 条 provider id**:新安装**默认开启 11 条、默认关闭 17 条**(数字与清单取自 [`lib/index.js`](lib/index.js) 顶部的 `PROVIDERS`,展示顺序与短名取自 [`lib/client.js`](lib/client.js) 顶部的 `PROVIDER_META` / `PROVIDER_ORDER`)。
|
|
103
126
|
|
|
104
127
|
| 提供商 | provider id | 额度类型 | 鉴权 | 默认 |
|
|
105
128
|
|---|---|---|---|---|
|
|
106
|
-
| DeepSeek | `deepseek` | 余额(账户) | Bearer,与推理同一把 Key | 开 |
|
|
129
|
+
| DeepSeek API | `deepseek` | 余额(账户) | Bearer,与推理同一把 Key | 开 |
|
|
130
|
+
| DeepSeek 账号 | `deepseek-account` | 余额(本机账号充值) | 复用 DSH `deepseekAccount` 登录态,无独立 Key | 开 |
|
|
107
131
|
| Z.ai(中国) | `zai-coding-cn` | 订阅窗口 | Bearer(团队档另加组织 / 项目请求头) | 开 |
|
|
108
132
|
| Kimi Coding | `kimi-coding` | 订阅窗口 | Bearer | 开 |
|
|
109
133
|
| Xiaomi MiMo | `xiaomi-token-plan-cn` | 周期额度池 | 官方平台 Cookie(含登录流程) | 开 |
|
|
@@ -156,7 +180,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
156
180
|
|
|
157
181
|
其余提供商**只经过桩测试**——响应形态取自公开文档、第三方实现或抓包,**没有在真实账号上跑通过**:
|
|
158
182
|
|
|
159
|
-
> Z.ai(中国与国际)、OpenCode Go、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、DeepSeek、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM,以及火山方舟的**团队版席位**。
|
|
183
|
+
> Z.ai(中国与国际)、OpenCode Go、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、DeepSeek API、DeepSeek 账号、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM,以及火山方舟的**团队版席位**。
|
|
160
184
|
|
|
161
185
|
它们可能可用,也可能因为接口变更、字段差异或权限要求而失败。上表也只是既有账号、当时版本的验证记录,不是持续可用性保证。
|
|
162
186
|
|
|
@@ -194,7 +218,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
194
218
|
|
|
195
219
|
```console
|
|
196
220
|
# npm 包名(推荐:预构建,免构建授权)
|
|
197
|
-
dsh plugin --profile
|
|
221
|
+
dsh plugin --profile web add dsh-subusage
|
|
198
222
|
|
|
199
223
|
# GitHub 固定 Commit(DSH STORE / awesome-dsh-plugin 只接受这种来源)
|
|
200
224
|
dsh plugin --profile <profile> add 'git+https://github.com/KouzakiUmi/dsh-subusage.git#<40 位完整 Commit>'
|
|
@@ -238,7 +262,7 @@ dsh plugin --profile <profile> remove dsh-subusage
|
|
|
238
262
|
|
|
239
263
|
- **设置页路径**:设置 → **订阅用量** → 提供商管理。「没有检测到API的默认隐藏」默认开启;首次没有可见条目时管理区自动展开。关闭的提供商仍可配置凭据,只是不会发起用量请求。
|
|
240
264
|
- **凭据从哪来**:每一类凭据的**官网入口、环境变量名与界面填入位置**都写在[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md)——包括火山的 IAM AK/SK(子用户还要挂 `ArkReadOnlyAccess` 且不限制到项目)、MiMo 的平台会话 Cookie、以及各家 API Key 的对照表。**注意推理 Key 与额度凭据不通用。**
|
|
241
|
-
- **默认开关策略**:新安装默认开启
|
|
265
|
+
- **默认开关策略**:新安装默认开启 11 条、默认关闭 17 条(**Codex 默认关闭**——它的提供方插件自带用量药丸);开关即时保存,升级保留已保存的开关,不重算默认值。默认隐藏只影响显示,不会自动开启被关闭的提供商。
|
|
242
266
|
- **火山方舟是一组**:4 条路由共用同一组 IAM AK/SK,所以设置页只呈现**一张卡片、一个开关**(本机已装的第一条路由作为代表,都没装则用第一条),卡片里写明本机实际装了哪几条。这些 provider id 并没有合并——药丸仍按 id 匹配路由。旧插件 `@volcengine/ark-plan-api` 的两个国内路由(`ark-coding-plan-cn` / `ark-agent-plan-cn`)**已移除**:它们与 arkcli 的路由查同一份订阅,并列显示只会让人以为买了两个套餐;装了旧插件的用户,药丸仍按那两个 id 显示同一份数据(入口归并到 arkcli 路由)。
|
|
243
267
|
- **火山方舟不按路由收起**:它的额度走**账号级管控面**,一组 AK/SK 就能查到名下所有套餐(`GetCodingPlanUsage` 与 `GetAFPUsage` 是同一套协议、同一组凭据)。推理路由装没装只影响模型能不能用,**不影响额度能不能查**——所以只持有 Coding Plan、却没在 DSH 里配 coding-plan 路由时,那条额度照样会显示(前提是配了 AK/SK)。
|
|
244
268
|
- **凭据来源优先级**(`inherit` 模式):**凭据服务 → 启动环境 → 旧手动配置兜底**;切到自定义(`manual`)模式时只使用保存的手动 Key。界面会区分这三种来源。
|
|
@@ -253,7 +277,8 @@ dsh plugin --profile <profile> remove dsh-subusage
|
|
|
253
277
|
|
|
254
278
|
| 模型商 | 默认继承变量 | 说明 |
|
|
255
279
|
|---|---|---|
|
|
256
|
-
| DeepSeek | `DEEPSEEK_API_KEY` | 余额型:`GET /user/balance` 返回账户余额,**与推理是同一把 Key** |
|
|
280
|
+
| DeepSeek API | `DEEPSEEK_API_KEY` | 余额型:`GET /user/balance` 返回账户余额,**与推理是同一把 Key** |
|
|
281
|
+
| DeepSeek 账号 | 无 | `deepseek-account` 路由;只读 DSH「账号与余额」中已登录账号的充值余额,**没有继承变量、也不使用独立 API Key**,请在 DSH 的账号与余额页面登录 |
|
|
257
282
|
| Z.ai(中国) | `ZAI_CODING_CN_API_KEY` | 团队套餐另填组织、项目 ID(作为 `bigmodel-organization` / `bigmodel-project` 请求头) |
|
|
258
283
|
| Z.ai(国际) | `ZAI_CODING_API_KEY` | 独立国际 Coding Plan Key,鉴权用裸 `authorization`,不带 `Bearer` |
|
|
259
284
|
| Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开放平台 Key |
|
|
@@ -325,7 +350,7 @@ arkcli-coding-plan-team ARKCLI_CODING_PLAN_TEAM_API_KEY
|
|
|
325
350
|
| `~/.grok/auth.json`(回退 `~/.dsh/.xai-oauth-auth.json`) | Grok CLI / dsh-grok-kit 的 OAuth 登录 | 平时只读;过期时经 grok-kit 官方链续约并写回同一文件,不自行裸刷 |
|
|
326
351
|
| `~/.commandcode/auth.json` | Command Code CLI(`cmd login`)的 Key | **只读**兜底 |
|
|
327
352
|
|
|
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 自动登录会另外打开官方平台页面,由你在该页面自行输入密码与验证码。**本插件没有自己的服务器,不做遥测,也不把你的数据发给任何第三方。**
|
|
353
|
+
**网络访问**:只连上面清单里各家自己的额度 / 余额端点,以及 `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
354
|
|
|
330
355
|
**命令、进程与安装期行为**
|
|
331
356
|
|
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,10 @@ 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
|
+
return BALANCE_ONLY_IDS.has(id) || !!PROVIDER_META[id]?.volc || !!result?.settings?.hasKeys?.[id] || result?.settings?.keyModes?.[id] === "manual" || providerRoutable(result, id);
|
|
421
|
+
}
|
|
411
422
|
function providerVisible(settings, id, entry, result) {
|
|
412
423
|
if (!providerEnabled(settings, id)) return false;
|
|
413
424
|
// 关掉「没有检测到 API 的默认隐藏」就是要看全部,不再按路由、凭据或订阅状态收起。
|
|
@@ -418,7 +429,9 @@ window.__ModuleLoader__.load({
|
|
|
418
429
|
// `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
|
|
419
430
|
if (PROVIDER_META[id]?.volc) {
|
|
420
431
|
if (entry?.subscribed === false) return false;
|
|
421
|
-
} else if (!
|
|
432
|
+
} else if (!providerCanQuery(result, id)) return false;
|
|
433
|
+
// 官方余额与本机账号余额独立于模型路由;刚保存 Key 的旧 no-key 条目不能继续隐藏。
|
|
434
|
+
if (settings?.hasKeys?.[id]) return true;
|
|
422
435
|
return entry?.apiDetected !== false && !["no-key", "no-cookie"].includes(entry?.state);
|
|
423
436
|
}
|
|
424
437
|
/** token 数紧凑格式:1,234,567,890 → 1.23B */
|
|
@@ -475,6 +488,24 @@ window.__ModuleLoader__.load({
|
|
|
475
488
|
const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
|
|
476
489
|
return row ? `${extraLabel(row, t)} ${row.value}` : null;
|
|
477
490
|
}
|
|
491
|
+
/** 余额只按自己的单位预警,不做汇率折算,也不把累计消费当余额。 */
|
|
492
|
+
function balanceVisual(entry, t) {
|
|
493
|
+
const row = Array.isArray(entry?.extras) ? entry.extras.find(x => x.kind === "balance" && typeof x.value === "string") : undefined;
|
|
494
|
+
if (!row) return null;
|
|
495
|
+
const text = row.value.trim();
|
|
496
|
+
const decimal = "([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?)";
|
|
497
|
+
const suffix = text.match(new RegExp("^" + decimal + "\\s+([A-Z]{3}|credits)$", "i"));
|
|
498
|
+
const prefix = text.match(new RegExp("^([¥¥$])\\s*" + decimal + "$", "i"));
|
|
499
|
+
const bare = text.match(new RegExp("^" + decimal + "$", "i"));
|
|
500
|
+
const amount = suffix ? Number(suffix[1]) : prefix ? Number(prefix[2]) : bare ? Number(bare[1]) : NaN;
|
|
501
|
+
const unit = suffix ? suffix[2].toUpperCase() : prefix ? prefix[1] === "$" ? "USD" : "CNY" : null;
|
|
502
|
+
const rawAmount = suffix?.[1] ?? prefix?.[2];
|
|
503
|
+
const value = unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text;
|
|
504
|
+
// 产品预警阈值,不是汇率或能运行多少次的保证。credits 没有跨厂商统一价值。
|
|
505
|
+
const limits = unit === "CNY" ? { red: 1, amber: 5 } : unit === "USD" ? { red: 0.2, amber: 1 } : null;
|
|
506
|
+
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") : "" };
|
|
508
|
+
}
|
|
478
509
|
/** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
|
|
479
510
|
function durationText(ms, t) {
|
|
480
511
|
const mins = Math.max(0, Math.round(ms / 60000));
|
|
@@ -787,7 +818,7 @@ window.__ModuleLoader__.load({
|
|
|
787
818
|
// 全部静默不发请求,用户点「刷新当前」也空转,只有「刷新全部」走 read 才能更新。
|
|
788
819
|
const unique = [...new Set(ids)].filter((id) => {
|
|
789
820
|
if (!PROVIDER_ORDER.includes(id) || !providerEnabled(snapshot.settings, id)) return false;
|
|
790
|
-
return
|
|
821
|
+
return providerCanQuery(snapshot, id);
|
|
791
822
|
});
|
|
792
823
|
const start = (id) => {
|
|
793
824
|
if (inflight.has(id)) return inflight.get(id);
|
|
@@ -1058,12 +1089,16 @@ window.__ModuleLoader__.load({
|
|
|
1058
1089
|
const worst = worstWindow(account.windows || []);
|
|
1059
1090
|
const tier = account.availability === "paygo" ? { color: "#eab308", icon: "ℹ" } : account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
|
|
1060
1091
|
const blocked = account.blocked;
|
|
1092
|
+
const limited = (account.windows || []).filter(w => w.status === "rate-limited");
|
|
1093
|
+
const roots = limited.filter(w => !w.cascade);
|
|
1094
|
+
const kinds = [...new Set(roots.length ? roots.map(w => w.kind) : limited.flatMap(w => w.detail?.blockedBy?.length ? w.detail.blockedBy : [w.kind]))];
|
|
1095
|
+
const limitDetail = kinds.length ? t("ccPoolWindowLimited").replace("{windows}", kinds.map(kind => t(KIND_LABEL[kind] || "ccPoolPeriodUnknown")).join(" / ")) : t("ccPoolLimitUnknown");
|
|
1061
1096
|
const unusable = !account.usable && !account.blocked;
|
|
1062
1097
|
const dot = statusGlyph(unusable ? NEUTRAL : (tier ? tier.color : NEUTRAL), unusable ? "?" : blocked ? "✕" : (tier ? tier.icon : "?"), 14);
|
|
1063
1098
|
const detail = unusable
|
|
1064
1099
|
? (account.availability === "unknown" ? t("statusUnknownShort") : account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
|
|
1065
1100
|
: blocked
|
|
1066
|
-
?
|
|
1101
|
+
? limitDetail
|
|
1067
1102
|
: account.availability === "paygo" ? t("statusPaygoShort") : worst ? pillText(worst, (account.windows || []).length > 1, t)
|
|
1068
1103
|
: t("noUsageData");
|
|
1069
1104
|
const canPin = cc && account.id !== "default";
|
|
@@ -1075,7 +1110,7 @@ window.__ModuleLoader__.load({
|
|
|
1075
1110
|
},
|
|
1076
1111
|
dot,
|
|
1077
1112
|
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: "
|
|
1113
|
+
h("span", { style: { opacity: 0.75, flex: "0 1 60%", minWidth: 0, textAlign: "right", whiteSpace: "normal", overflowWrap: "anywhere" } }, detail));
|
|
1079
1114
|
};
|
|
1080
1115
|
return h("div", { style: { marginTop: 10, padding: "8px 10px", borderRadius: 10, border: `1px solid ${alpha(tone, ".35")}`, background: alpha(tone, ".07") } },
|
|
1081
1116
|
h("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, fontWeight: 600, color: textTone(tone) } },
|
|
@@ -1084,7 +1119,9 @@ window.__ModuleLoader__.load({
|
|
|
1084
1119
|
h("span", { style: { fontWeight: 500, opacity: 0.9 } }, `${pool.available}/${pool.total}`)),
|
|
1085
1120
|
h("p", { role: pool.exhausted ? "alert" : "status", style: { margin: "3px 0 0", fontSize: 11, color: textTone(tone) } }, summary),
|
|
1086
1121
|
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("
|
|
1122
|
+
h("details", { style: { marginTop: 4 } },
|
|
1123
|
+
h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } }, t("ccPoolDetails")),
|
|
1124
|
+
h("div", { style: { maxHeight: 240, overflowY: "auto", marginTop: 4 } }, pool.accounts.map(rowFor))));
|
|
1088
1125
|
}
|
|
1089
1126
|
|
|
1090
1127
|
// ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
|
|
@@ -1153,7 +1190,7 @@ window.__ModuleLoader__.load({
|
|
|
1153
1190
|
const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
|
|
1154
1191
|
return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
|
|
1155
1192
|
};
|
|
1156
|
-
const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
|
|
1193
|
+
const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : providerId === "deepseek-account" ? t("pillNeedDeepseekAccount") : t("pillNeedKey"))
|
|
1157
1194
|
: state === "no-cookie" ? t("pillNeedCookie") : null;
|
|
1158
1195
|
const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
|
|
1159
1196
|
if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
|
|
@@ -1165,6 +1202,8 @@ window.__ModuleLoader__.load({
|
|
|
1165
1202
|
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
1203
|
if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
|
|
1167
1204
|
const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
|
|
1205
|
+
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 };
|
|
1168
1207
|
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
1208
|
}
|
|
1170
1209
|
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 +1226,7 @@ window.__ModuleLoader__.load({
|
|
|
1187
1226
|
// 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
|
|
1188
1227
|
const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
|
|
1189
1228
|
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 };
|
|
1229
|
+
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
1230
|
}
|
|
1192
1231
|
if (worstTier) {
|
|
1193
1232
|
const sub = windowSub(worst);
|
|
@@ -1197,8 +1236,8 @@ window.__ModuleLoader__.load({
|
|
|
1197
1236
|
}
|
|
1198
1237
|
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
1238
|
// 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
|
|
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 };
|
|
1239
|
+
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 };
|
|
1202
1241
|
}
|
|
1203
1242
|
/** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
|
|
1204
1243
|
function statusBanner(status) {
|
|
@@ -1370,10 +1409,10 @@ window.__ModuleLoader__.load({
|
|
|
1370
1409
|
const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
|
|
1371
1410
|
// 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
|
|
1372
1411
|
// 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 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 : ""}`;
|
|
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 : ""}`;
|
|
1374
1413
|
// 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
|
|
1375
1414
|
// 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
|
|
1376
|
-
const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
|
|
1415
|
+
const glyph = windows.length ? miniBars(windows, degraded) : st.icon === "" ? null : statusGlyph(st.color, st.icon, 16);
|
|
1377
1416
|
return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
|
|
1378
1417
|
h("button", {
|
|
1379
1418
|
type: "button",
|
|
@@ -1386,7 +1425,7 @@ window.__ModuleLoader__.load({
|
|
|
1386
1425
|
display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
|
|
1387
1426
|
borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
|
|
1388
1427
|
background: st.tinted ? alpha(st.color, ".10") : "transparent",
|
|
1389
|
-
color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
|
|
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"
|
|
1390
1429
|
}
|
|
1391
1430
|
},
|
|
1392
1431
|
glyph,
|
|
@@ -1702,7 +1741,7 @@ window.__ModuleLoader__.load({
|
|
|
1702
1741
|
const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
|
|
1703
1742
|
: 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
1743
|
: 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 };
|
|
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") };
|
|
1706
1745
|
const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
|
|
1707
1746
|
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
1747
|
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,10 +1757,11 @@ window.__ModuleLoader__.load({
|
|
|
1718
1757
|
h("div", { style: { marginTop: 10 } },
|
|
1719
1758
|
h("fieldset", { disabled: busy || !result.settings || typeof result.settings.revision !== "string", style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
|
|
1720
1759
|
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,
|
|
1760
|
+
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
1761
|
tab === "synthetic" ? h("p", null, text("使用 Synthetic API Key,仅显示模型订阅请求额度。", "Use a Synthetic API key. Shows the model subscription request quota.")) : null,
|
|
1722
1762
|
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
1763
|
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,
|
|
1764
|
+
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,
|
|
1725
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,
|
|
1726
1766
|
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
1767
|
// 用户通常已经登录着控制台,直接给入口比让他自己翻菜单快得多。
|
|
@@ -1789,7 +1829,7 @@ window.__ModuleLoader__.load({
|
|
|
1789
1829
|
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
1830
|
h("span", { style: { flex: 1, minWidth: 80 } }, title),
|
|
1791
1831
|
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") : !
|
|
1832
|
+
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
1833
|
h("details", { id: `subusage-credentials-${id}`, open: editing, style: { width: "100%" } },
|
|
1794
1834
|
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
1835
|
tab === id ? credentialEditor : null));
|
package/lib/index.js
CHANGED
|
@@ -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.12.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
|
};
|
|
@@ -518,18 +520,25 @@ export function normalizeZai(body) {
|
|
|
518
520
|
return { windows: cascadeRateLimited(windows), level: typeof data.level === "string" ? data.level : undefined };
|
|
519
521
|
}
|
|
520
522
|
export function normalizeMimo(balBody, detailBody, usageBody) {
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
+
const rawBal = balBody?.data;
|
|
524
|
+
const bal = rawBal && typeof rawBal.balance === "string" && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(rawBal.balance) && Number.isFinite(Number(rawBal.balance)) ? rawBal : undefined;
|
|
523
525
|
const data = usageBody?.data ?? {}; const detail = detailBody?.data ?? {};
|
|
524
526
|
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
|
|
527
|
+
const pools = [pick(data.usage, "plan_total_token"), pick(data.monthUsage, "month_total_token")].filter(Boolean);
|
|
528
|
+
const pool = pools.find(row => row.limit !== 0) ?? pools[0];
|
|
529
|
+
const groups = ["usage", "monthUsage"].filter(key => Object.hasOwn(data, key)).map(key => data[key]);
|
|
530
|
+
const emptyGroups = groups.length > 0 && groups.every(group => group === null || Array.isArray(group?.items) && group.items.length === 0);
|
|
531
|
+
const detailReadable = detailBody?.data === null || detailBody?.data && typeof detailBody.data === "object" && !Array.isArray(detailBody.data);
|
|
532
|
+
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));
|
|
533
|
+
const poolDetail = numeric(pool?.used) && numeric(pool?.limit) && pool.limit > 0 ? { used: pool.used, limit: pool.limit, unit: "credits" } : undefined;
|
|
534
|
+
const windows = pool && pool.limit !== 0 && numeric(pool.percent) ? [windowRow("sub", pool.percent * 100, detail.currentPeriodEnd, undefined, poolDetail)] : [];
|
|
535
|
+
const currency = typeof bal?.currency === "string" && /^[A-Z]{3}$/.test(bal.currency) ? bal.currency : "";
|
|
536
|
+
const extras = bal ? [{ kind: "balance", value: `${bal.balance} ${currency}`.trim() }] : [];
|
|
537
|
+
if (!bal && !windows.length) throw failure("subusage/response", "MiMo 没有可读的余额或套餐用量");
|
|
538
|
+
if (!bal) extras.push({ kind: "note", value: "余额暂不可读;套餐用量已更新" });
|
|
530
539
|
const plan = detail.planName || detail.planCode;
|
|
531
540
|
if (typeof plan === "string") extras.push({ kind: "plan", value: plan });
|
|
532
|
-
return { windows, extras, balanceOk: true, coverage: windows.length ? "complete" : "partial" };
|
|
541
|
+
return { windows, extras, balanceOk: !!bal, ...(windows.length ? { subscribed: true } : noSubscription ? { subscribed: false } : {}), coverage: windows.length || noSubscription && !!bal ? "complete" : "partial" };
|
|
533
542
|
}
|
|
534
543
|
export function normalizeOpencodeGo(body) {
|
|
535
544
|
const src = body?.usage ?? body;
|
|
@@ -734,7 +743,7 @@ export function normalizeCodexUsage(body) {
|
|
|
734
743
|
const credits = body?.credits && typeof body.credits === "object" && !Array.isArray(body.credits) ? body.credits : {};
|
|
735
744
|
const balance = typeof credits.balance === "string" && credits.balance.trim() ? credits.balance.trim() : typeof credits.balance === "number" ? String(credits.balance) : "";
|
|
736
745
|
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
|
|
746
|
+
if (credits.has_credits === true && credits.unlimited !== true && creditBalance !== undefined) extras.push({ kind: "balance", value: balance });
|
|
738
747
|
if (credits.has_credits === true && credits.unlimited === true) extras.push({ kind: "credit-status", value: "unlimited" });
|
|
739
748
|
const control = body?.spend_control;
|
|
740
749
|
const member = control?.individual_limit;
|
|
@@ -759,6 +768,15 @@ export function normalizeCodexUsage(body) {
|
|
|
759
768
|
// DeepSeek 官方余额(余额型):GET https://api.deepseek.com/user/balance,Bearer 与推理同一把 Key。
|
|
760
769
|
// 响应 { is_available, balance_infos: [{ currency, total_balance, granted_balance, topped_up_balance }] },
|
|
761
770
|
// 金额是**字符串十进制**。余额没有上限也就没有百分比:不产出窗口,只产出 extras。
|
|
771
|
+
// DSH 账号服务已完成鉴权与余额响应投影;本插件不读取账号 grant/token。
|
|
772
|
+
export function normalizeDeepseekAccountBalance(body) {
|
|
773
|
+
const decimal = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
774
|
+
if (body?.status !== "ready" || !Array.isArray(body.value)) throw failure("subusage/response", "本机 DeepSeek 账号余额读取失败,请检查 DSH 的账号与余额页面", true);
|
|
775
|
+
const wallets = body.value.filter(row => ["CNY", "USD"].includes(row?.currency) && typeof row.balance === "string" && decimal.test(row.balance));
|
|
776
|
+
if (!wallets.length || wallets.length !== body.value.length) throw failure("subusage/response", "本机 DeepSeek 账号没有有效的充值余额信息");
|
|
777
|
+
const row = wallets.find(row => row.currency === "CNY") ?? wallets[0];
|
|
778
|
+
return { windows: [], extras: [{ kind: "balance", value: `${row.balance} ${row.currency}` }], coverage: "complete" };
|
|
779
|
+
}
|
|
762
780
|
export function normalizeDeepseekBalance(body) {
|
|
763
781
|
if (typeof body?.is_available !== "boolean") throw failure("subusage/response", "Invalid DeepSeek balance response");
|
|
764
782
|
const infos = Array.isArray(body.balance_infos) ? body.balance_infos : [];
|
|
@@ -928,7 +946,7 @@ export function normalizeLitellm(keyBody, budgetBody) {
|
|
|
928
946
|
return { windows, extras, coverage: "complete" };
|
|
929
947
|
}
|
|
930
948
|
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)])); }
|
|
949
|
+
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
950
|
catch { return undefined; }
|
|
933
951
|
}
|
|
934
952
|
const codec = (type, parse) => ({ mode: "strict", typeSymbol: `dsh-subusage#${type}`, schema: { parse }, create: () => ({ parse }) });
|
|
@@ -977,6 +995,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
977
995
|
// identity 解析(凭据服务/登录文件读取)与 HTTP 同量级的超时,默认 10s(对齐 fetchJson 的
|
|
978
996
|
// AbortSignal.timeout);经 options 注入以便测试用极短值,不必真等 10 秒。
|
|
979
997
|
this.identityTimeoutMs = options.identityTimeoutMs ?? 10000;
|
|
998
|
+
this.accountBalanceTimeoutMs = options.accountBalanceTimeoutMs ?? 10000;
|
|
999
|
+
this.deepseekAccount = options.resolveDeepseekAccount ?? (() => ctx.get?.("deepseekAccount"));
|
|
980
1000
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
981
1001
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
982
1002
|
// 火山方舟的 AK/SK 不是路由的推理 Key,按固定变量名各自解析,两端都命中才算配对。
|
|
@@ -1035,7 +1055,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1035
1055
|
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}) } : { key: undefined, source: "none" };
|
|
1036
1056
|
}
|
|
1037
1057
|
if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
|
|
1038
|
-
if (id === "
|
|
1058
|
+
if (id === "deepseek-account") {
|
|
1059
|
+
const service = this.deepseekAccount();
|
|
1060
|
+
const ready = typeof service?.getState === "function" && typeof service?.getBalance === "function" && (await service.getState())?.status === "credential-stored";
|
|
1061
|
+
return { key: undefined, source: ready ? "account-service" : "none" };
|
|
1062
|
+
}
|
|
1063
|
+
if (id === "commandcode" && account && account !== "default") {
|
|
1039
1064
|
// 额外账户:id 即凭据引用名(apiKeyEnv)。引用名不合法(不可能来自提供方
|
|
1040
1065
|
// 插件的 slots,但 client 输入不可信)按未配置处理,不让 credentialRef 抛错。
|
|
1041
1066
|
if (!isCredentialRefName(account)) return { key: undefined, source: "none" };
|
|
@@ -1175,6 +1200,13 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1175
1200
|
if (id === "minimax" || id === "minimax-cn") return normalizeMinimax(await get(PROVIDERS[id].usageUrl, headers));
|
|
1176
1201
|
if (id === "siliconflow") return normalizeSiliconFlow(await get("https://api.siliconflow.cn/v1/user/info", headers));
|
|
1177
1202
|
if (id === "deepseek") return normalizeDeepseekBalance(await get("https://api.deepseek.com/user/balance", headers));
|
|
1203
|
+
if (id === "deepseek-account") {
|
|
1204
|
+
const service = this.deepseekAccount();
|
|
1205
|
+
if (typeof service?.getBalance !== "function") throw failure("subusage/response", "本机 DeepSeek 账号余额服务不可用", true);
|
|
1206
|
+
const body = await withTimeout(service.getBalance({ version: VERSION, locale: "zh-CN", timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 }), this.accountBalanceTimeoutMs, () => failure("subusage/network", "本机 DeepSeek 账号余额读取超时", true));
|
|
1207
|
+
if (body === null) throw failure("subusage/auth", "本机 DeepSeek 账号未登录或登录已失效,请在 DSH 账号与余额页面确认");
|
|
1208
|
+
return normalizeDeepseekAccountBalance(body);
|
|
1209
|
+
}
|
|
1178
1210
|
if (id === "openrouter") {
|
|
1179
1211
|
const keyBody = await get("https://openrouter.ai/api/v1/key", headers);
|
|
1180
1212
|
// 余额端点对普通推理 key 会 403;这一次失败不影响 key 限额与用量的展示。
|
|
@@ -1195,7 +1227,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1195
1227
|
get("https://zenmux.ai/api/v1/management/subscription/detail", headers).catch(() => undefined),
|
|
1196
1228
|
get("https://zenmux.ai/api/v1/management/payg/balance", headers).catch(() => undefined)
|
|
1197
1229
|
]);
|
|
1198
|
-
|
|
1230
|
+
const data = normalizeZenmux(subscription, balance);
|
|
1231
|
+
if (subscription === undefined) {
|
|
1232
|
+
data.coverage = "partial";
|
|
1233
|
+
data.extras.push({ kind: "note", value: "余额已读取;套餐状态暂不可确认" });
|
|
1234
|
+
}
|
|
1235
|
+
return data;
|
|
1199
1236
|
}
|
|
1200
1237
|
if (id === "litellm") {
|
|
1201
1238
|
// 地址是用户自己的 proxy;缺地址算「凭据不完整」,与缺 Key 一样给出可行动的指引。
|
|
@@ -1258,7 +1295,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1258
1295
|
const base = "https://platform.xiaomimimo.com/api/v1";
|
|
1259
1296
|
const h = { cookie: identity.key, origin: "https://platform.xiaomimimo.com", referer: "https://platform.xiaomimimo.com/#/console/balance", "x-timezone": "UTC+08:00" };
|
|
1260
1297
|
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; })]);
|
|
1298
|
+
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
1299
|
// 普通刷新与登录验证都经过这里:服务端字段不可信,缓存前去除 Cookie 回显。
|
|
1263
1300
|
const data = redactMimo(normalizeMimo(bal, detail, usage), identity.key);
|
|
1264
1301
|
if (usageError) data.usageError = usageError;
|
|
@@ -1383,7 +1420,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1383
1420
|
if (startedGeneration !== (this.generations.get(cacheKey) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
1384
1421
|
if (!providerEnabled(settings, id)) {
|
|
1385
1422
|
this.cache.delete(cacheKey); this.inflight.delete(cacheKey);
|
|
1386
|
-
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
1423
|
+
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key || identity.source === "account-service", windows: [], extras: [] };
|
|
1387
1424
|
}
|
|
1388
1425
|
// fingerprint 计入账户选择:切换账户后同 id 缓存/inflight 不串账(不同账户 key 必不同);
|
|
1389
1426
|
// 空串与 "default" 同为默认账户,归一后共享缓存。
|
|
@@ -1393,7 +1430,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1393
1430
|
const generation = this.generations.get(cacheKey) ?? 0;
|
|
1394
1431
|
const cached = this.cache.get(cacheKey);
|
|
1395
1432
|
if (cached && cached.fingerprint !== fingerprint) this.cache.delete(cacheKey);
|
|
1396
|
-
|
|
1433
|
+
// 账号服务不提供会话指纹,也不读取其 token:每次查询复用其实时余额链,不跨账号复用本插件缓存。
|
|
1434
|
+
const previous = identity.source !== "account-service" && cached?.fingerprint === fingerprint ? cached : undefined;
|
|
1397
1435
|
const active = this.inflight.get(cacheKey);
|
|
1398
1436
|
if (active?.fingerprint === fingerprint && active.generation === generation) return active.promise;
|
|
1399
1437
|
const now = this.now();
|
|
@@ -1401,7 +1439,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1401
1439
|
if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
|
|
1402
1440
|
const promise = (async () => {
|
|
1403
1441
|
const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
1404
|
-
if (!identity.key) entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
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" };
|
|
1405
1443
|
else try {
|
|
1406
1444
|
const data = await this.fetchProvider(id, identity, settings);
|
|
1407
1445
|
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.12.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",
|