dsh-subusage 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -84
- package/lib/client.js +42 -17
- package/lib/index.js +30 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,124 +1,99 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/title.png" width="100%" alt="dsh-subusage —— 模型选择器旁的余量药丸,右侧是同一界面的浅色与深色用量弹层(真实组件离线渲染示例)">
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
# 订阅用量显示
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
**额度与余额,不必离开对话。**
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
在 DeepSeek Harness 的模型选择器旁看余量;点开药丸,查看额度窗口、重置时间与余额。
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
|
+
[](#license--security--许可与安全)
|
|
11
|
+
[](#compatibility--兼容性)
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
---
|
|
13
|
+
[快速开始](#快速开始) · [功能预览](#功能预览) · [支持清单](#支持的数据--supported-data) · [凭据指引](docs/credentials.md)
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| 没有它 | 有了它 |
|
|
22
|
-
|---|---|
|
|
23
|
-
| 额度散落在五六家控制台,查一次余额要开五个标签页 | 药丸常驻输入框旁:选中哪家模型就看哪家,多窗口自动取最差一窗 |
|
|
24
|
-
| Agent 跑到一半撞上 429,才知道额度用尽 | 弹层先给结论再给依据:「即将用尽 · 最紧张:每周 · 剩 5% · 3 天后重置」 |
|
|
25
|
-
| Cookie、OAuth 悄悄过期,报错才发现 | MiMo Cookie 提前 2 小时变黄、30 分钟变橙、过期变红;认证失效直说原因与修法 |
|
|
26
|
-
| 每家插件各管各的凭据,格式五花八门 | 一个设置页集中管理 28 条路由的开关与凭据,缺什么补什么 |
|
|
15
|
+
<img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/title.png" width="1200" alt="dsh-subusage:保留原标题图的深蓝构图,右侧展示真实 Codex 与 OpenCode Go 深浅主题用量弹层;虚构数据离线预览">
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
<sub>真实组件 · 虚构数据 · 真实组件离线预览,不是运行中的 DSH 截图</sub>
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/screenshots/pill-states-light.png" width="100%" alt="药丸 15 种状态(浅色,离线预览)">
|
|
33
|
-
|
|
34
|
-
<details>
|
|
35
|
-
<summary>深色主题</summary>
|
|
19
|
+
</div>
|
|
36
20
|
|
|
37
|
-
|
|
21
|
+
> **0.13.0**:余额显示更清晰,设置按需刷新,兼容 Codex Subscription 独立 OAuth。发布与本机升级分开;本页图片是虚构数据离线预览,不是实机验收证明。[查看更新记录](docs/changelog.md)。
|
|
38
22
|
|
|
39
|
-
|
|
23
|
+
## 快速开始
|
|
40
24
|
|
|
41
|
-
|
|
25
|
+
```console
|
|
26
|
+
dsh plugin --profile web add dsh-subusage
|
|
27
|
+
```
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
|---|---|---|
|
|
45
|
-
| 余量药丸 | 不用切到各家控制台,选中模型就知道这家还剩多少 | 模型选择器左侧,槽位 `conversation.input.right` |
|
|
46
|
-
| 设置页 | 开关与凭据集中管理;缺凭据、Cookie 过期时把修复入口直接送到卡片上 | 设置 → 订阅用量 → 提供商管理 |
|
|
47
|
-
| 只读额度 API | 其他插件与 Agent 复用同一份额度数据,不必各自对接各家接口 | Agent 工具 `subusage_quota`、Host 服务 `subUsage.quota()` |
|
|
29
|
+
`web` 是示例 Profile,请替换为实际目标。**Desktop 使用目标桌面壳提供的 DSH Desktop CLI**,不要从独立公共 CLI 管理保留的 `desktop` Profile。
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
1. **启用插件**:按部署方式重载,让 Host 与 Client 一起生效;只刷新页面不保证 Host 已升级。
|
|
32
|
+
2. **配置需要的提供商**:打开 **设置 → 订阅用量 → 提供商管理**。填 API Key,或使用对应提供方插件的登录态;MiMo 可“登录并自动导入”。[不知道该填什么?](docs/credentials.md)
|
|
33
|
+
3. **回到对话**:选择该提供商的模型,输入区出现用量药丸。点开看明细;读取失败会说明原因,不把未知当充足。
|
|
50
34
|
|
|
51
|
-
|
|
35
|
+
**已经登录 DSH 的 DeepSeek 账号?** 默认开启的「DeepSeek 账号」直接读取宿主余额,不需要复制 token。独立 API Key 使用另一个「DeepSeek API」入口,两份余额不相加、不互相替代。[两种入口的区别](docs/providers.md#deepseek-api-与-deepseek-账号)
|
|
52
36
|
|
|
53
|
-
##
|
|
37
|
+
## 功能预览
|
|
54
38
|
|
|
55
|
-
|
|
39
|
+
<picture>
|
|
40
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/readme/features-dark.png">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/KouzakiUmi/dsh-subusage/main/assets/readme/features-light.png" width="1200" alt="三块真实组件:Codex 套餐用量、独立 DeepSeek 账号余额、Command Code 多账户轮换池;虚构数据离线预览">
|
|
42
|
+
</picture>
|
|
56
43
|
|
|
57
|
-
###
|
|
44
|
+
### 看额度:最紧张的窗口,先提醒你
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
一个窗口一根余量条。正常时保持简洁;周窗偏少、额度耗尽、读取失败时,药丸明确提示。点开后再看百分比、适用范围与重置倒计时,不需要先切到控制台。
|
|
60
47
|
|
|
61
|
-
|
|
62
|
-
- **余额药丸分级**:药丸直接显示金额与单位,不再一律中性灰。人民币 < 1 元红、1–5 元黄、≥ 5 元灰;美元 < 0.2 红、0.2–1 黄、≥ 1 灰;`credits` 及其它无统一价值单位只在已解析的非正余额时标红,正余额保持灰色而不按绝对量虚构价值。这些是**产品预警阈值,不是汇率换算,也不是「还能跑多少次」的保证**。累计花费不套用余额阈值。
|
|
63
|
-
- **新增 `deepseek-account`(DeepSeek 账号)**:默认开启,复用 DSH 本机 `deepseekAccount` 服务的登录态读取充值余额;不读取、导出或保存 OAuth token,也不使用独立 API Key。与 `deepseek`(DeepSeek API)的开关、缓存和余额完全独立;`deepseek-official` 这个名字指向账号入口。纯余额型条目(两个 DeepSeek 入口与 SiliconFlow、Novita、Hyperbolic、DeepInfra、Vercel AI Gateway)不要求本机装有对应模型路由。
|
|
64
|
-
- **MiMo 无订阅不再画成 0%**:只有「用量组为空 / 零容量」配合可读的套餐详情才判未订阅;零容量不生成 0% 用量窗口。套餐读取失败仍标部分数据,余额读取失败仍保留已成功的用量。
|
|
65
|
-
- **ZenMux 订阅端点失败**保留余额并标部分数据,明确写「套餐状态暂不可确认」,不宣称未订阅。
|
|
48
|
+
### 看余额:金额直接显示,单位不混淆
|
|
66
49
|
|
|
67
|
-
|
|
50
|
+
无套餐窗口且数据完整时,药丸直接显示金额。人民币、美元与 credits 各按自己的规则展示;**余额不等于订阅额度,也不能盖过已耗尽的套餐**。DeepSeek API 与本机账号有各自的开关、凭据来源与余额。
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
<summary>余额药丸与套餐优先:0.12.0 深色离线预览</summary>
|
|
52
|
+
### 管账户:需要时展开,不让长列表占满界面
|
|
71
53
|
|
|
72
|
-
|
|
54
|
+
Command Code 可在弹层和设置页切换固定账户,或查看自动轮换池的健康度。逐账户详情默认折叠,受限周期明确标注。**自动轮换模式的用量仍是默认账户,不跟随实际服务中的账户。**
|
|
73
55
|
|
|
74
|
-
|
|
56
|
+
## 支持什么
|
|
75
57
|
|
|
76
|
-
|
|
58
|
+
**28 条数据入口,新安装默认开启 11 条。** 按使用场景,主要分为:
|
|
77
59
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
以上均已通过离线回归;不代表真实企业席位、新旧私有接口或付费资格已经在线验收。详细契约与仍待核验范围见[开发说明](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md#额度语义修正0112)。
|
|
60
|
+
| 你想看什么 | 代表提供商 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| 订阅窗口与重置时间 | Kimi、MiMo、Codex、SuperGrok、MiniMax、Z.ai、OpenCode Go |
|
|
63
|
+
| 账号钱包与 API 余额 | DeepSeek API / 账号、SiliconFlow、Novita、Hyperbolic、DeepInfra |
|
|
64
|
+
| 套餐、余额与多层预算 | Command Code、NanoGPT、OpenRouter、ZenMux、LiteLLM |
|
|
65
|
+
| 账号级套餐与企业席位 | 火山方舟 Agent Plan / Coding Plan |
|
|
85
66
|
|
|
86
|
-
[
|
|
67
|
+
[查看完整支持清单](#支持的数据--supported-data) · [查看哪些经过真实账号验证](#support-status--实测覆盖)
|
|
87
68
|
|
|
88
|
-
|
|
69
|
+
本插件**只显示用量,不注册模型路由**。Codex 默认关闭,避免与提供方自带药丸重复;需要统一展示时可手动开启。私有额度接口可能变化,数字仅供参考,不是计费依据。
|
|
89
70
|
|
|
90
|
-
|
|
91
|
-
dsh plugin --profile web add dsh-subusage
|
|
92
|
-
```
|
|
71
|
+
## 也可以直接问 Agent
|
|
93
72
|
|
|
94
|
-
|
|
73
|
+
“看一下 Kimi 和 Codex 还剩多少额度。”
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
2. 打开 **设置 → 订阅用量 → 提供商管理**,开启需要的提供商并填凭据——每类凭据的官网入口与填入位置见[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md);MiMo 用「登录并自动导入」。
|
|
98
|
-
3. 回到会话,选中该提供商的模型:输入区左侧出现药丸,点开看窗口明细。
|
|
99
|
-
4. 排障:点「立即刷新」→ 看设置页顶部 `X/Y 家数据获取成功`(可点击,直接跳到第一家没读成功的)。
|
|
75
|
+
模型可调用只读工具 `subusage_quota`,读取与界面同源的额度与余额;可按提供商筛选,Command Code 可分别返回各账户状态。不会通过该工具修改设置。[工具与 Host 服务契约](#额度查询-api)
|
|
100
76
|
|
|
101
|
-
|
|
77
|
+
## 使用前,先知道这三点
|
|
102
78
|
|
|
103
|
-
|
|
79
|
+
- **不猜数据**:查询失败、缓存与部分覆盖会明确标注;接口读取成功不保证仍可续用。
|
|
80
|
+
- **不把凭据放进截图或结果**:API Key、Cookie 与 OAuth token 不回填界面;但本插件手动配置仍可能明文落盘,**它不是加密凭据库**。
|
|
81
|
+
- **验证有范围**:历史真实账号记录覆盖 7 个入口,其余主要依靠桩测试;新图是虚构数据离线渲染,不能作为真实账号在线验收证明。[实测范围与风险](#support-status--实测覆盖)
|
|
104
82
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
$env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需按部署方式重启
|
|
108
|
-
# 或在 设置 → 订阅用量 → DeepSeek API →「连接与凭据」里手动填写
|
|
109
|
-
```
|
|
83
|
+
<details>
|
|
84
|
+
<summary><strong>版本与验证范围:本页截图展示了什么?</strong></summary>
|
|
110
85
|
|
|
111
|
-
|
|
86
|
+
标题图和三功能特写由当前 `lib/client.js` 的真实组件生成,未制作平行 UI。画面使用虚构金额与账户;绿色余额图标、最多两位小数与独立 Codex Subscription OAuth 兼容纳入 **0.13.0**。OAuth 续约仍由凭据所属插件负责;未替代真实账号在线验收。
|
|
112
87
|
|
|
113
|
-
|
|
88
|
+
0.12.0 已发布双 DeepSeek 入口、分单位余额预警与 Command Code 详情折叠;该版本正常余额为灰色。历史截图与每轮验证范围见[更新记录](docs/changelog.md)和[开发说明](docs/development.md)。
|
|
114
89
|
|
|
115
|
-
|
|
90
|
+
</details>
|
|
116
91
|
|
|
117
92
|
---
|
|
118
93
|
|
|
119
94
|
# 技术文档
|
|
120
95
|
|
|
121
|
-
|
|
96
|
+
安装、配置与排障参考。日常使用先看上面的快速开始,需要定位具体接口或凭据来源时再展开下文。
|
|
122
97
|
|
|
123
98
|
## 支持的数据 / Supported data
|
|
124
99
|
|
|
@@ -134,7 +109,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
134
109
|
| OpenCode Go | `opencode-go` | 订阅窗口 | Bearer | 开 |
|
|
135
110
|
| Command Code | `commandcode` | 5 小时 / 周 / 月额度池 | 提供方插件的凭据链 | 开 |
|
|
136
111
|
| SuperGrok | `xai-oauth` | 周期池 | 共享 Grok CLI 登录文件,过期经 grok-kit 续约 | 开 |
|
|
137
|
-
| Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Codex CLI
|
|
112
|
+
| Codex (ChatGPT) | `openai-codex` | ChatGPT 订阅窗口 | 只读 Subscription 独立 OAuth(0.13.0 起)或 Codex CLI 登录文件 | 关 |
|
|
138
113
|
| MiniMax(中国) | `minimax-cn` | 短周期 / 周 | Bearer | 开 |
|
|
139
114
|
| ARK Agent Plan (arkcli) | `arkcli-agent-plan` | 订阅窗口(AFP) | IAM AK/SK 签名 | 开 |
|
|
140
115
|
| ARK Coding Plan (arkcli) | `arkcli-coding-plan` | 订阅窗口 | IAM AK/SK 签名 | 开 |
|
|
@@ -252,7 +227,7 @@ dsh plugin --profile <profile> remove dsh-subusage
|
|
|
252
227
|
| --- | --- | --- |
|
|
253
228
|
| Command Code | [`@mars-sea/dsh-commandcode-provider`](https://www.npmjs.com/package/@mars-sea/dsh-commandcode-provider) | 在该插件的设置页登录,或运行 `cmd login`(兜底读 `~/.commandcode/auth.json`) |
|
|
254
229
|
| SuperGrok | [`dsh-grok-kit`](https://github.com/KouzakiUmi/dsh-grok-kit) | 运行 `grok login`,登录文件 `~/.grok/auth.json` |
|
|
255
|
-
| Codex(ChatGPT 订阅) | [`dsh-codex-connect`](https://github.com/franksong2702/dsh-codex-connect)(
|
|
230
|
+
| Codex(ChatGPT 订阅) | [`dsh-codex-connect`](https://github.com/franksong2702/dsh-codex-connect) 或 [`dsh-codex-subscription`](https://github.com/WSL043/dsh-codex-subscription)(0.13.0 起兼容) | Connect / CLI:`codex login`;Subscription:在其设置页独立 OAuth 登录,无需 CLI |
|
|
256
231
|
|
|
257
232
|
> **Codex 默认关闭**:它的提供方插件本身就带一个用量药丸,两个并排只是重复信息。想要本插件这一份(比如想把 Codex 和别家放在一起看)可以在**提供商管理**里手动开启。
|
|
258
233
|
>
|
|
@@ -288,7 +263,7 @@ dsh plugin --profile <profile> remove dsh-subusage
|
|
|
288
263
|
| MiniMax(中国) | `MINIMAX_CN_API_KEY` | `minimax-cn` 路由;中国站订阅 Key |
|
|
289
264
|
| Command Code | `COMMANDCODE_API_KEY` | 凭据由提供方插件管理,本页只读继承;兜底读取 `~/.commandcode/auth.json`(`cmd login`) |
|
|
290
265
|
| SuperGrok | 无 | `xai-oauth` 路由;读取 dsh-grok-kit / Grok CLI 共享的 OAuth 登录文件 `~/.grok/auth.json`(旧版 `~/.dsh/.xai-oauth-auth.json`),不保存凭据。access token 过期时**借 dsh-grok-kit(≥0.2.0)的官方登录链自动续约**——同一文件、同一把写锁、compare-and-write,不会顶掉共享登录态(本插件绝不自行裸 POST 刷新);未安装 grok-kit 或续约失败时才提示手动重新登录。API Key(`XAI_API_KEY`)取不到订阅周池 |
|
|
291
|
-
| Codex | 无 | `openai-codex`
|
|
266
|
+
| Codex | 无 | `openai-codex` 路由;0.13.0 起优先只读 Subscription 活动 OAuth 账号,再回退 CLI 文件(`$CODEX_HOME/auth.json` 或 `~/.codex/auth.json`,要求 `auth_mode` 为 `chatgpt`);不保存或刷新 token,续约由所属插件负责 |
|
|
292
267
|
| 火山方舟 Ark(4 条路由) | `VOLC_ACCESSKEY` + `VOLC_SECRETKEY` | 额度查询要 **IAM Access Key 的 AK/SK 配对**,与推理 API Key(`ARKCLI_*_API_KEY` / `ARK_*_API_KEY`)**不是同一套**;4 条路由共用一组 AK/SK。这一组 AK/SK 能**同时**查到 Agent Plan 与 Coding Plan(含团队版席位)——不存在「Coding Plan 专用查询 key」 |
|
|
293
268
|
| SiliconFlow | `SILICONFLOW_API_KEY` | 余额型:`GET /v1/user/info` 返回账户余额,**与推理是同一把 Key** |
|
|
294
269
|
| OpenRouter | `OPENROUTER_API_KEY` | key 限额与用量用推理 key 即可;**账户余额需要 management / provisioning key**,普通 key 会被 403 拒——此时静默降级为只显示限额,不判失败 |
|
|
@@ -467,7 +442,7 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
|
|
|
467
442
|
|
|
468
443
|
文档入口:[凭据获取指引](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/credentials.md)(每类凭据的官网入口、环境变量与填入位置、报错对照)、[开发与验证](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/development.md)(RPC 契约、缓存与凭据约定、离线预览与实机验收范围)、[提供商清单与额度语义](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/providers.md)、[提供商覆盖与取舍](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/provider-coverage.md)、[UX 设计](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/design-ux.md)、[发布与市场收录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/publish.md)、[更新记录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/changelog.md)、[审查记录](https://github.com/KouzakiUmi/dsh-subusage/blob/main/docs/code-review.md)。
|
|
469
444
|
|
|
470
|
-
> 打包说明:`package.json` 的 `files` 白名单是 `lib` / `cordis.patch.yml` / `README.md` / `LICENSE`,因此 `assets/title.
|
|
445
|
+
> 打包说明:`package.json` 的 `files` 白名单是 `lib` / `cordis.patch.yml` / `README.md` / `LICENSE`,因此 `assets/title.png`、`assets/readme/`、`assets/screenshots/` 与 `docs/` **不进 npm 包**——图片通过 GitHub 绝对地址加载,npm 页面无需把图片打进包也能显示。改动白名单属于发布决策,需单独处理。
|
|
471
446
|
|
|
472
447
|
### 界面预览
|
|
473
448
|
|
|
@@ -481,7 +456,16 @@ node scripts/check-browser-runtime.mjs # 需要工作区可解析 playwright-c
|
|
|
481
456
|
|
|
482
457
|

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

|
|
463
|
+
|
|
464
|
+

|
|
465
|
+
|
|
466
|
+
</details>
|
|
467
|
+
|
|
468
|
+
0.13.0 的功能特写见[功能预览](#功能预览)。
|
|
485
469
|
|
|
486
470
|
截图声明见根目录 [`screenshots.json`](screenshots.json)。上述四张保留 0.10.11 的基础界面预览;下方三张为 0.11.2 新增语义状态的真实组件离线预览(虚构用量、桩网络、无凭据),底部水印明确标注「非运行中的 DSH 截图」。真实 DSH Loader、实际账号在线接口与多账户映射仍未验收,截图不代表已通过这些检查。
|
|
487
471
|
|
package/lib/client.js
CHANGED
|
@@ -417,7 +417,8 @@ window.__ModuleLoader__.load({
|
|
|
417
417
|
function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
|
|
418
418
|
const BALANCE_ONLY_IDS = new Set(["deepseek", "deepseek-account", "siliconflow", "novita", "hyperbolic", "deepinfra", "vercel-ai-gateway"]);
|
|
419
419
|
function providerCanQuery(result, id) {
|
|
420
|
-
|
|
420
|
+
const detected = id === "openai-codex" && result?.entries?.find(entry => entry.providerId === id)?.apiDetected === true;
|
|
421
|
+
return BALANCE_ONLY_IDS.has(id) || !!PROVIDER_META[id]?.volc || detected || !!result?.settings?.hasKeys?.[id] || result?.settings?.keyModes?.[id] === "manual" || providerRoutable(result, id);
|
|
421
422
|
}
|
|
422
423
|
function providerVisible(settings, id, entry, result) {
|
|
423
424
|
if (!providerEnabled(settings, id)) return false;
|
|
@@ -484,9 +485,19 @@ window.__ModuleLoader__.load({
|
|
|
484
485
|
return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : ["note", "quota-status"].includes(x.kind) ? t("noteLabel") : x.kind === "credit-status" ? t("creditStatusLabel") : x.kind === "spend" || x.kind.endsWith("-spend") ? t("spendLabel") : t("balanceLabel");
|
|
485
486
|
}
|
|
486
487
|
/** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
|
|
488
|
+
function formatBalanceValue(value) {
|
|
489
|
+
if (typeof value !== "string") return value;
|
|
490
|
+
const match = value.trim().match(/^([¥¥$]?\s*)([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(\s*(?:[A-Z]{3}|credits)?)$/i);
|
|
491
|
+
if (!match || !Number.isFinite(Number(match[2]))) return value;
|
|
492
|
+
if (!/[eE]/.test(match[2]) && (match[2].split(".")[1]?.length ?? 0) <= 2) return value.trim();
|
|
493
|
+
const amount = Number(match[2]);
|
|
494
|
+
// 仅收敛展示精度;状态阈值仍使用未经四舍五入的原始金额。
|
|
495
|
+
const formatted = String(Number(amount.toFixed(2)));
|
|
496
|
+
return match[1] + formatted + match[3];
|
|
497
|
+
}
|
|
487
498
|
function balanceText(entry, t) {
|
|
488
499
|
const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
|
|
489
|
-
return row ? `${extraLabel(row, t)} ${row.value}` : null;
|
|
500
|
+
return row ? `${extraLabel(row, t)} ${row.kind === "balance" ? formatBalanceValue(row.value) : row.value}` : null;
|
|
490
501
|
}
|
|
491
502
|
/** 余额只按自己的单位预警,不做汇率折算,也不把累计消费当余额。 */
|
|
492
503
|
function balanceVisual(entry, t) {
|
|
@@ -500,11 +511,11 @@ window.__ModuleLoader__.load({
|
|
|
500
511
|
const amount = suffix ? Number(suffix[1]) : prefix ? Number(prefix[2]) : bare ? Number(bare[1]) : NaN;
|
|
501
512
|
const unit = suffix ? suffix[2].toUpperCase() : prefix ? prefix[1] === "$" ? "USD" : "CNY" : null;
|
|
502
513
|
const rawAmount = suffix?.[1] ?? prefix?.[2];
|
|
503
|
-
const value = unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text;
|
|
514
|
+
const value = formatBalanceValue(unit === "CNY" ? `¥${rawAmount}` : unit === "USD" ? `$${rawAmount}` : text);
|
|
504
515
|
// 产品预警阈值,不是汇率或能运行多少次的保证。credits 没有跨厂商统一价值。
|
|
505
516
|
const limits = unit === "CNY" ? { red: 1, amber: 5 } : unit === "USD" ? { red: 0.2, amber: 1 } : null;
|
|
506
517
|
const rank = Number.isFinite(amount) ? amount <= 0 || limits && amount < limits.red ? 2 : limits && amount < limits.amber ? 1 : 0 : 0;
|
|
507
|
-
return { value, color: rank === 2 ? "#ef4444" : rank === 1 ? "#eab308" : "#94a3b8", tinted: rank > 0, label: rank > 0 ? t("balanceLow") : "" };
|
|
518
|
+
return { value, color: rank === 2 ? "#ef4444" : rank === 1 ? "#eab308" : limits ? "#22c55e" : "#94a3b8", icon: rank > 0 ? "!" : limits ? "✓" : "ℹ", tinted: rank > 0, label: rank > 0 ? t("balanceLow") : "" };
|
|
508
519
|
}
|
|
509
520
|
/** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
|
|
510
521
|
function durationText(ms, t) {
|
|
@@ -782,7 +793,7 @@ window.__ModuleLoader__.load({
|
|
|
782
793
|
/** 每次 apply 一个 store;partial response 只合并请求项,所有槽共享 inflight 与活动计时。 */
|
|
783
794
|
function createUsageStore(call) {
|
|
784
795
|
let snapshot = { updatedAt: null, settings: null, configured: {}, entries: [] };
|
|
785
|
-
let disposed = false, initial = null, timer = null, epoch = 0, sequence = 0, metadataSequence = 0;
|
|
796
|
+
let disposed = false, initial = null, settingsInitial = null, timer = null, epoch = 0, sequence = 0, metadataSequence = 0;
|
|
786
797
|
let loginGeneration = 0, loginJobId = null, loginPending = false;
|
|
787
798
|
// Command Code 显示账户:弹层账户区选中的账户("" = 默认账户/顶层 Key)。
|
|
788
799
|
// 所有 commandcode 读取(readAll/refresh/定时刷新)都携带它,切换后用量同步跟随。
|
|
@@ -883,6 +894,15 @@ window.__ModuleLoader__.load({
|
|
|
883
894
|
};
|
|
884
895
|
return {
|
|
885
896
|
subscribe, getSnapshot: () => snapshot, refresh, activate,
|
|
897
|
+
// 设置页先取本机元数据,不等待所有厂商的网络额度请求。
|
|
898
|
+
readSettings: () => {
|
|
899
|
+
if (snapshot.settings) return Promise.resolve(snapshot);
|
|
900
|
+
if (!settingsInitial) {
|
|
901
|
+
const request = markRequest([]);
|
|
902
|
+
settingsInitial = Promise.resolve().then(() => call("read", { providerIds: [], force: false, commandCodeAccount: ccAccount })).then(value => merge(value, request)).catch(error => { settingsInitial = null; throw error; });
|
|
903
|
+
}
|
|
904
|
+
return settingsInitial.then(() => snapshot);
|
|
905
|
+
},
|
|
886
906
|
getCommandCodeAccount: () => ccAccount, setCommandCodeAccount,
|
|
887
907
|
startMimoLogin: async (request) => { const generation = ++loginGeneration, startedEpoch = epoch; loginJobId = null; return acceptLogin(await call("startMimoLogin", request), generation, startedEpoch); },
|
|
888
908
|
getMimoLoginStatus: async () => { const generation = loginGeneration, startedEpoch = epoch, jobId = loginPending ? loginJobId : null; return acceptLogin(await call("getMimoLoginStatus"), generation, startedEpoch, jobId); },
|
|
@@ -1203,7 +1223,7 @@ window.__ModuleLoader__.load({
|
|
|
1203
1223
|
if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
|
|
1204
1224
|
const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
|
|
1205
1225
|
const wallet = !windows.length && rawEntry.coverage === "complete" && rawEntry.availability === "paygo" ? balanceVisual(rawEntry, t) : null;
|
|
1206
|
-
if (wallet && !expiryActionable) return { ...base, key: "paygo", color: wallet.color, icon:
|
|
1226
|
+
if (wallet && !expiryActionable) return { ...base, key: "paygo", color: wallet.color, icon: wallet.icon, title: balanceText(rawEntry, t), sub: [title, wallet.label].filter(Boolean).join(" · "), short: wallet.value, tinted: wallet.tinted };
|
|
1207
1227
|
return { ...base, key: rawEntry.availability, color: "#eab308", icon: rawEntry.availability === "unknown" ? "?" : "ℹ", title, sub: rawEntry.availability === "paygo" ? t("statusPaygoSub") : worst ? windowSub(worst) : "", short: t(rawEntry.availability === "paygo" ? "statusPaygoShort" : rawEntry.availability === "included" ? "statusIncludedShort" : "statusUnknownShort"), tinted: true };
|
|
1208
1228
|
}
|
|
1209
1229
|
const scopeLimited = worstTier?.rank >= 4 && windows.some(w => w.status !== "rate-limited" && w.detail?.scope && w.detail.scope !== "all") && windows.filter(w => w.status === "rate-limited").every(w => w.detail?.scope && w.detail.scope !== "all");
|
|
@@ -1237,7 +1257,7 @@ window.__ModuleLoader__.load({
|
|
|
1237
1257
|
if (entry.extras?.some(e => e.kind === "quota-status" && e.value === "unlimited")) return { ...base, key: "unlimited", color: NEUTRAL, icon: "ℹ", title: t("statusUnlimited"), sub: "", short: t("statusUnlimited"), tinted: false };
|
|
1238
1258
|
// 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
|
|
1239
1259
|
const balance = balanceText(entry, t), visual = balanceVisual(entry, t);
|
|
1240
|
-
return { ...base, key: "balance", color: visual?.color ?? NEUTRAL, icon: visual
|
|
1260
|
+
return { ...base, key: "balance", color: visual?.color ?? NEUTRAL, icon: visual?.icon ?? "ℹ", title: balance ?? t("noUsageData"), sub: visual?.label ?? "", short: visual?.value ?? balance ?? t("noUsageData"), tinted: visual?.tinted ?? false };
|
|
1241
1261
|
}
|
|
1242
1262
|
/** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
|
|
1243
1263
|
function statusBanner(status) {
|
|
@@ -1409,7 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1409
1429
|
const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
|
|
1410
1430
|
// 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
|
|
1411
1431
|
// 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
|
|
1412
|
-
const tip = `${label}: ${st.title}${
|
|
1432
|
+
const tip = `${label}: ${st.title}${["balance", "paygo"].includes(st.key) && st.sub ? " · " + st.sub : ""}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
|
|
1413
1433
|
// 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
|
|
1414
1434
|
// 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
|
|
1415
1435
|
const glyph = windows.length ? miniBars(windows, degraded) : st.icon === "" ? null : statusGlyph(st.color, st.icon, 16);
|
|
@@ -1425,7 +1445,7 @@ window.__ModuleLoader__.load({
|
|
|
1425
1445
|
display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
|
|
1426
1446
|
borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
|
|
1427
1447
|
background: st.tinted ? alpha(st.color, ".10") : "transparent",
|
|
1428
|
-
color:
|
|
1448
|
+
color: ["balance", "paygo"].includes(st.key) && st.tinted ? `color-mix(in srgb, ${st.color} 65%, currentColor)` : "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
|
|
1429
1449
|
}
|
|
1430
1450
|
},
|
|
1431
1451
|
glyph,
|
|
@@ -1469,7 +1489,7 @@ window.__ModuleLoader__.load({
|
|
|
1469
1489
|
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1470
1490
|
entry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1471
1491
|
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1472
|
-
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1492
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.kind === "balance" ? formatBalanceValue(x.value) : x.value))))
|
|
1473
1493
|
: null,
|
|
1474
1494
|
providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
|
|
1475
1495
|
// 页脚:更新时间 + 刷新。
|
|
@@ -1565,14 +1585,14 @@ window.__ModuleLoader__.load({
|
|
|
1565
1585
|
const text = (cn, en) => local ? cn : en;
|
|
1566
1586
|
react.useEffect(() => {
|
|
1567
1587
|
let alive = true;
|
|
1568
|
-
void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
|
|
1588
|
+
void (usageStore.readSettings ?? usageStore.readAll)().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
|
|
1569
1589
|
return () => { alive = false; };
|
|
1570
1590
|
}, [usageStore]);
|
|
1571
1591
|
react.useEffect(() => {
|
|
1572
1592
|
let alive = true;
|
|
1573
1593
|
const enabled = editorOpen && providerEnabled(result.settings, tab);
|
|
1574
1594
|
const deactivate = enabled ? usageStore.activate(tab) : () => {};
|
|
1575
|
-
void usageStore.readAll().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
|
|
1595
|
+
void (usageStore.readSettings ?? usageStore.readAll)().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
|
|
1576
1596
|
return () => { alive = false; deactivate(); };
|
|
1577
1597
|
}, [usageStore, tab, editorOpen, providerEnabled(result.settings, tab)]);
|
|
1578
1598
|
react.useEffect(() => {
|
|
@@ -1632,7 +1652,7 @@ window.__ModuleLoader__.load({
|
|
|
1632
1652
|
};
|
|
1633
1653
|
const refresh = async (all) => {
|
|
1634
1654
|
setRefreshing(true);
|
|
1635
|
-
try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
|
|
1655
|
+
try { if (!result.settings) await (usageStore.readSettings ?? usageStore.readAll)(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
|
|
1636
1656
|
catch (error) { if (aliveRef.current) setLoadError(localError(error.code, String(error.message || error), t)); }
|
|
1637
1657
|
finally { if (aliveRef.current) setRefreshing(false); }
|
|
1638
1658
|
};
|
|
@@ -1741,7 +1761,7 @@ window.__ModuleLoader__.load({
|
|
|
1741
1761
|
const authFileLabel = tab === "xai-oauth" ? text("来自 ~/.grok/auth.json(Grok / Grok Kit 登录)", "From ~/.grok/auth.json (Grok / Grok Kit sign-in)")
|
|
1742
1762
|
: tab === "openai-codex" ? text("来自 $CODEX_HOME/auth.json 或 ~/.codex/auth.json(codex login)", "From $CODEX_HOME/auth.json or ~/.codex/auth.json (codex login)")
|
|
1743
1763
|
: text("来自 ~/.commandcode/auth.json(cmd login)", "From ~/.commandcode/auth.json (cmd login)");
|
|
1744
|
-
const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel, "account-service": text("本机 DeepSeek 账号余额", "Local DeepSeek account balance") };
|
|
1764
|
+
const sources = { credentials: text("来自凭据服务", "From credential service"), env: text("来自环境变量", "From environment"), manual: text("手动 Key", "Manual key"), none: text("无可用凭据", "No credentials"), cookie: text("官方平台 Cookie", "Platform Cookie"), "auth-file": authFileLabel, "account-service": text("本机 DeepSeek 账号余额", "Local DeepSeek account balance"), "subscription-credentials": text("Codex Subscription 活动账号(DSH 凭据服务)", "Codex Subscription active account (DSH credentials)") };
|
|
1745
1765
|
const credentialButtons = (kind, hasSaved) => h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
|
|
1746
1766
|
draft[kind + "Action"] !== "replace" ? h("button", { type: "button", style: btnStyle, disabled: kind === "cookie" && loginActive, onClick: () => editDraft({ [kind + "Action"]: "replace", [kind]: "", ...(kind === "key" ? { keyMode: "manual" } : {}) }) }, text(kind === "cookie" ? "手动导入" : "更换", kind === "cookie" ? "Manual import" : "Change")) : null,
|
|
1747
1767
|
hasSaved && draft[kind + "Action"] !== "clear" ? h("button", { type: "button", style: btnStyle, disabled: kind === "cookie" && loginActive, onClick: () => { editDraft({ [kind + "Action"]: "clear", [kind]: "" }); setNotice({ ok: false, text: text("待清除:保存设置后将删除已保存的敏感凭据;取消编辑可保留。", "Pending removal: saving will delete the saved secret. Cancel edits to keep it.") }); } }, text(kind === "cookie" ? "清除登录凭据" : "清除", kind === "cookie" ? "Clear login credentials" : "Clear")) : null,
|
|
@@ -1762,7 +1782,7 @@ window.__ModuleLoader__.load({
|
|
|
1762
1782
|
tab === "nanogpt" ? h("p", null, text("可用 NanoGPT API Key,推荐手动填写 Usage only 管理令牌(sk-nano-mgmt-…)。显示日/周输入 Token 与试用额度,图像额度单独计算,不纳入此处。", "Use a NanoGPT API key or, preferably, a manual Usage only management token (sk-nano-mgmt-…). Shows daily/weekly input tokens and trial quotas; image quotas are separate."), " ", h("a", { href: "https://nano-gpt.com/settings", target: "_blank", rel: "noopener noreferrer" }, text("打开官方设置", "Open official settings"))) : null,
|
|
1763
1783
|
h("p", null, `${result.configured && result.configured[tab] ? t("detected") : t("notDetected")} · ${sources[source] || sources.none}`),
|
|
1764
1784
|
tab === "deepseek-account" ? h("p", null, text("只读 DSH「账号与余额」中已登录账号的充值余额。默认开启;不读取、导出或保存 OAuth token,不使用独立 API Key。请在 DSH 的账号与余额页面登录。", "Reads the signed-in account's recharge balance from DSH Account & balance. Enabled by default; never reads, exports or stores OAuth tokens, and never uses an independent API key. Sign in on DSH Account & balance.")) : tab === "commandcode" ? h(react.Fragment, null, h("p", null, t("commandcodeManaged"))) : tab === "xai-oauth" ? h(react.Fragment, null, h("p", null, t("grokManaged"))) : tab === "openai-codex" ? h(react.Fragment, null,
|
|
1765
|
-
h("p", null, text("
|
|
1785
|
+
h("p", null, text("优先只读 Codex Subscription 的独立 OAuth 活动账号,无需 CLI;没有该来源再读 Codex CLI 的 ChatGPT 订阅登录($CODEX_HOME/auth.json 或 ~/.codex/auth.json)。本页不保存凭据、不刷新 token;续约由凭据所属插件负责。请在 Subscription 设置页登录,或用 codex login;API Key 模式(auth_mode 不是 chatgpt)取不到订阅额度。", "Reads the Codex Subscription active OAuth account first (no CLI required), then the Codex CLI ChatGPT login ($CODEX_HOME/auth.json or ~/.codex/auth.json). This page never saves or refreshes tokens; renewal belongs to the owning plugin. Sign in in Subscription settings or with codex login; API-key mode (auth_mode not chatgpt) cannot read subscription quota."))) : PROVIDER_META[tab].volc ? h(react.Fragment, null,
|
|
1766
1786
|
h("p", null, text("额度查询要用火山引擎 IAM 的 Access Key(AK/SK 配对,权限建议只给方舟只读),与推理用的方舟 API Key 不是同一套;Ark 的这几条路由共用这一组凭据。", "Quota lookup needs a Volcengine IAM Access Key pair (Ark read-only is enough). This is NOT the Ark inference API key; every Ark route here shares this pair.")),
|
|
1767
1787
|
// 用户通常已经登录着控制台,直接给入口比让他自己翻菜单快得多。
|
|
1768
1788
|
h("p", { style: { fontSize: 12 } }, externalLink("https://console.volcengine.com/iam/keymanage", text("打开火山引擎「API 访问密钥」创建 AK/SK", "Open Volcengine “API access keys”")), " · ", credentialsGuide),
|
|
@@ -1892,7 +1912,7 @@ window.__ModuleLoader__.load({
|
|
|
1892
1912
|
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1893
1913
|
usageEntry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1894
1914
|
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1895
|
-
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1915
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.kind === "balance" ? formatBalanceValue(x.value) : x.value))))
|
|
1896
1916
|
: null,
|
|
1897
1917
|
// 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
|
|
1898
1918
|
// 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
|
|
@@ -1962,7 +1982,12 @@ window.__ModuleLoader__.load({
|
|
|
1962
1982
|
/** 槽包装:先看当前模型的 provider,再分发到对应药丸。 */
|
|
1963
1983
|
function SubusagePillEntry({ store, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
|
|
1964
1984
|
const state = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1965
|
-
const
|
|
1985
|
+
const lastProvider = react.useRef(null);
|
|
1986
|
+
const currentProvider = state.current?.provider;
|
|
1987
|
+
// 宿主模型目录在重连/重建期间会以 loading/error + current:null 发布快照。
|
|
1988
|
+
// 只在这类暂态保留已知选择;ready 的空选择或显式换厂商不能沿用。
|
|
1989
|
+
if (currentProvider || !["loading", "error"].includes(state.status)) lastProvider.current = currentProvider ?? null;
|
|
1990
|
+
const provider = currentProvider ?? (["loading", "error"].includes(state.status) ? lastProvider.current : null);
|
|
1966
1991
|
// 旧插件 @volcengine/ark-plan-api 的两个国内路由 id 不在本表里,但它们是**同一个 plan**:
|
|
1967
1992
|
// 不管模型路由由哪个插件提供,都归并到 arkcli 路由再查——所以只会有**一个**面板,
|
|
1968
1993
|
// 数据来自同一份订阅(实测两条路由的请求 URL 逐字相同)。
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// dsh-subusage Host:秘密仅保存在 Host,远端只暴露 provider 补丁和公开设置。
|
|
2
2
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
-
import { credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
import { credentialKey, credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
|
|
5
5
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
6
6
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -11,7 +11,7 @@ import { MimoLogin } from "./mimo-login.js";
|
|
|
11
11
|
import { parseVolcAgentPlan, parseVolcCodingPlan, parseVolcSeatAfp, parseVolcSeatCoding, parseVolcSeatIds, volcError, volcSignature } from "./volcengine.js";
|
|
12
12
|
|
|
13
13
|
const name = "dsh-subusage";
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.13.0";
|
|
15
15
|
const inject = ["llm"];
|
|
16
16
|
const PROVIDERS = {
|
|
17
17
|
"zai-coding-cn": { label: "Z.ai", envName: "ZAI_CODING_CN_API_KEY" },
|
|
@@ -267,6 +267,30 @@ function grokAuthFileCredentials(io) {
|
|
|
267
267
|
// 与 Grok 一样只读:token 由 Codex CLI 自己轮换,第三端刷新会互相顶掉。
|
|
268
268
|
const CODEX_AUTH_DIR = join(homedir(), ".codex");
|
|
269
269
|
const CODEX_API_BASE = "https://chatgpt.com/backend-api";
|
|
270
|
+
// dsh-codex-subscription 的活动账号记录是权威来源;只读,不迁移或写回第三方 OAuth。
|
|
271
|
+
export async function codexSubscriptionCredentials(credentials) {
|
|
272
|
+
if (!credentials) return undefined;
|
|
273
|
+
const normalize = value => {
|
|
274
|
+
if (value?.type !== "oauth" || typeof value.access !== "string" || !value.access.trim() || typeof value.accountId !== "string" || !value.accountId.trim() || !Number.isFinite(value.expires)) throw failure("subusage/credential", "Codex Subscription 登录凭据格式异常,请在该插件设置页检查登录");
|
|
275
|
+
return { key: value.access, accountId: value.accountId, expiresAt: value.expires, source: "subscription-credentials" };
|
|
276
|
+
};
|
|
277
|
+
const record = await credentials.readRecord?.(credentialKey("codex-subscription", "accounts"));
|
|
278
|
+
if (record !== undefined) {
|
|
279
|
+
const payload = record?.payload;
|
|
280
|
+
if (record?.kind !== "grant" || payload?.version !== 1 || typeof payload.activeId !== "string" || !Array.isArray(payload.accounts)) throw failure("subusage/credential", "Codex Subscription 账号记录格式异常");
|
|
281
|
+
const selected = payload.accounts.filter(account => account?.id === payload.activeId);
|
|
282
|
+
if (selected.length !== 1) throw failure("subusage/credential", "Codex Subscription 活动账号不存在或重复");
|
|
283
|
+
return normalize(selected[0].credential);
|
|
284
|
+
}
|
|
285
|
+
for (const name of ["OPENAI_CODEX_SUBSCRIPTION_OAUTH", "WSL043_OPENAI_CODEX_OAUTH"]) {
|
|
286
|
+
const hit = await credentials.resolve?.(credentialRef(name));
|
|
287
|
+
if (!hit?.value) continue;
|
|
288
|
+
let value;
|
|
289
|
+
try { value = JSON.parse(hit.value); } catch { throw failure("subusage/credential", "Codex Subscription 登录凭据不是有效 JSON"); }
|
|
290
|
+
return normalize(value);
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
270
294
|
export function codexAuthFileCredentials(io, env) {
|
|
271
295
|
const home = typeof env?.CODEX_HOME === "string" && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : CODEX_AUTH_DIR;
|
|
272
296
|
try {
|
|
@@ -1017,7 +1041,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1017
1041
|
// 过期时借 dsh-grok-kit 的官方会话链刷新同一登录文件;解析不到兄弟插件时返回 undefined 并降级。
|
|
1018
1042
|
this.resolveGrokKit = options.resolveGrokKit ?? loadGrokKit;
|
|
1019
1043
|
// Codex 只读共享登录文件($CODEX_HOME/auth.json 或 ~/.codex/auth.json)取 OAuth access token。
|
|
1020
|
-
this.codexAuth = options.resolveCodexAuth ?? (async () => codexAuthFileCredentials(this.io, { CODEX_HOME: (await this.environmentByName("CODEX_HOME"))?.value }));
|
|
1044
|
+
this.codexAuth = options.resolveCodexAuth ?? (async () => (await codexSubscriptionCredentials(ctx.get?.("credentials"))) ?? codexAuthFileCredentials(this.io, { CODEX_HOME: (await this.environmentByName("CODEX_HOME"))?.value }));
|
|
1021
1045
|
this.cache = new Map(); this.inflight = new Map(); this.generations = new Map(); this.fingerprints = new Map(); this.disposed = false; this.controller = new AbortController();
|
|
1022
1046
|
this.login = new MimoLogin({
|
|
1023
1047
|
error: failure,
|
|
@@ -1050,9 +1074,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1050
1074
|
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.userId ? { userId: cred.userId } : {}), ...(cred.expiresAt ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
|
|
1051
1075
|
}
|
|
1052
1076
|
if (id === "openai-codex") {
|
|
1053
|
-
//
|
|
1077
|
+
// 优先只读 Subscription 活动 OAuth,再回落 CLI;续约由凭据所属插件负责。
|
|
1054
1078
|
const cred = await this.codexAuth();
|
|
1055
|
-
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}) } : { key: undefined, source: "none" };
|
|
1079
|
+
return cred?.key ? { key: cred.key, source: cred.source ?? "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}), ...(Number.isFinite(cred.expiresAt) ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
|
|
1056
1080
|
}
|
|
1057
1081
|
if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
|
|
1058
1082
|
if (id === "deepseek-account") {
|
|
@@ -1439,7 +1463,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1439
1463
|
if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
|
|
1440
1464
|
const promise = (async () => {
|
|
1441
1465
|
const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
1442
|
-
if (!identity.key && identity.source !== "account-service") entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "deepseek-account" ? "未检测到本机 DeepSeek 账号登录:请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key" : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex
|
|
1466
|
+
if (!identity.key && identity.source !== "account-service") entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "deepseek-account" ? "未检测到本机 DeepSeek 账号登录:请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key" : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:在 Codex Subscription 设置页独立登录,或运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
1443
1467
|
else try {
|
|
1444
1468
|
const data = await this.fetchProvider(id, identity, settings);
|
|
1445
1469
|
const usageError = data.usageError; delete data.usageError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"displayName": "订阅用量显示",
|
|
5
5
|
"description": "显示独立 DeepSeek API Key 与 DSH 本机账号余额(账号入口默认开启)、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
6
6
|
"keywords": [
|