dsh-subusage 0.10.5 → 0.10.7
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 +7 -10
- package/lib/client.js +30 -10
- package/lib/index.js +58 -14
- package/lib/volcengine.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **English.** dsh-subusage shows subscription quota and balance for the AI providers you already use, as a pill next to the model selector in DeepSeek Harness. Provider switches and credentials live in one settings page, and other plugins or agents can read the same snapshot through a read-only quota API.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
10
|
[](#license--security--许可与安全)
|
|
11
11
|
[](#compatibility--兼容性)
|
|
12
12
|
|
|
@@ -45,8 +45,6 @@
|
|
|
45
45
|
| MiniMax(国际) | `minimax` | 短周期 / 周 | Bearer | 关 |
|
|
46
46
|
| ARK Agent Plan Team (arkcli) | `arkcli-agent-plan-team` | 席位额度(AFP) | IAM AK/SK 签名 | 关 |
|
|
47
47
|
| ARK Coding Plan Team (arkcli) | `arkcli-coding-plan-team` | 席位额度 | IAM AK/SK 签名 | 关 |
|
|
48
|
-
| Ark Coding Plan(中国,旧插件路由) | `ark-coding-plan-cn` | 订阅窗口 | IAM AK/SK 签名 | 关 |
|
|
49
|
-
| Ark Agent Plan(中国,旧插件路由) | `ark-agent-plan-cn` | 订阅窗口(AFP) | IAM AK/SK 签名 | 关 |
|
|
50
48
|
| Ark Coding Plan(BytePlus,旧插件路由) | `ark-coding-plan-byteplus` | 订阅窗口 | IAM AK/SK 签名 | 关 |
|
|
51
49
|
| SiliconFlow | `siliconflow` | 余额 | Bearer,与推理同一把 Key | 关 |
|
|
52
50
|
| OpenRouter | `openrouter` | 限额窗口 + 余额 | 推理 Key(余额需 management / provisioning key) | 关 |
|
|
@@ -142,7 +140,8 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
142
140
|
- **设置页路径**:设置 → **订阅用量** → 提供商管理。「没有检测到API的默认隐藏」默认开启;首次没有可见条目时管理区自动展开。关闭的提供商仍可配置凭据,只是不会发起用量请求。
|
|
143
141
|
- **凭据从哪来**:每一类凭据的**官网入口、环境变量名与界面填入位置**都写在[凭据获取指引](docs/credentials.md)——包括火山的 IAM AK/SK(子用户还要挂 `ArkReadOnlyAccess` 且不限制到项目)、MiMo 的平台会话 Cookie、以及各家 API Key 的对照表。**注意推理 Key 与额度凭据不通用。**
|
|
144
142
|
- **默认开关策略**:新安装默认开启 10 条、默认关闭 20 条(**Codex 默认关闭**——它的提供方插件自带用量药丸);开关即时保存,升级保留已保存的开关,不重算默认值。默认隐藏只影响显示,不会自动开启被关闭的提供商。
|
|
145
|
-
- **火山方舟是一组**:
|
|
143
|
+
- **火山方舟是一组**:5 条路由共用同一组 IAM AK/SK,所以设置页只呈现**一张卡片、一个开关**(组内最后选的那条作为代表),卡片里写明本机实际装了哪几条。这些 provider id 并没有合并——药丸仍按 id 匹配路由。旧插件 `@volcengine/ark-plan-api` 的两个国内路由(`ark-coding-plan-cn` / `ark-agent-plan-cn`)**已移除**:它们与 arkcli 的路由查同一份订阅,并列显示只会让人以为买了两个套餐;装了旧插件的用户,药丸仍按那两个 id 显示同一份数据(入口归并到 arkcli 路由)。
|
|
144
|
+
- **火山方舟不按路由收起**:它的额度走**账号级管控面**,一组 AK/SK 就能查到名下所有套餐(`GetCodingPlanUsage` 与 `GetAFPUsage` 是同一套协议、同一组凭据)。推理路由装没装只影响模型能不能用,**不影响额度能不能查**——所以只持有 Coding Plan、却没在 DSH 里配 coding-plan 路由时,那条额度照样会显示(前提是配了 AK/SK)。
|
|
146
145
|
- **凭据来源优先级**(`inherit` 模式):**凭据服务 → 启动环境 → 旧手动配置兜底**;切到自定义(`manual`)模式时只使用保存的手动 Key。界面会区分这三种来源。
|
|
147
146
|
- **不接受本地 Key 的提供商**:`commandcode`(凭据链属提供方插件)、`xai-oauth`、`openai-codex`(只读各自 CLI 的登录文件),以及 MiMo(Cookie 会话)。
|
|
148
147
|
- **保存与验证分离**:凭据先确认持久化,再独立做在线验证;检测关闭的提供商只保存凭据、不发起验证,验证失败不代表保存失败。
|
|
@@ -164,9 +163,9 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
164
163
|
| MiniMax(国际) | `MINIMAX_API_KEY` | `minimax` 路由;国际站订阅 Key |
|
|
165
164
|
| MiniMax(中国) | `MINIMAX_CN_API_KEY` | `minimax-cn` 路由;中国站订阅 Key |
|
|
166
165
|
| Command Code | `COMMANDCODE_API_KEY` | 凭据由提供方插件管理,本页只读继承;兜底读取 `~/.commandcode/auth.json`(`cmd login`) |
|
|
167
|
-
| SuperGrok | 无 | `xai-oauth` 路由;只读 dsh-grok-kit / Grok CLI 共享的 OAuth 登录文件 `~/.grok/auth.json`(旧版 `~/.dsh/.xai-oauth-auth.json
|
|
166
|
+
| SuperGrok | 无 | `xai-oauth` 路由;只读 dsh-grok-kit / Grok CLI 共享的 OAuth 登录文件 `~/.grok/auth.json`(旧版 `~/.dsh/.xai-oauth-auth.json`),不保存、**不刷新 token**;access token 过期时直接说明过期时间并提示去刷(不代刷——refresh-token 轮换由 Grok CLI / grok-kit 各自的锁协议管理,第三端刷新会把它们的轮换顶掉)。API Key(`XAI_API_KEY`)取不到订阅周池 |
|
|
168
167
|
| Codex | 无 | `openai-codex` 路由;只读 Codex CLI 的 ChatGPT 订阅登录文件(`$CODEX_HOME/auth.json` 或 `~/.codex/auth.json`,要求 `auth_mode` 为 `chatgpt`),不保存、不刷新 token |
|
|
169
|
-
| 火山方舟 Ark(
|
|
168
|
+
| 火山方舟 Ark(5 条路由) | `VOLC_ACCESSKEY` + `VOLC_SECRETKEY` | 额度查询要 **IAM Access Key 的 AK/SK 配对**,与推理 API Key(`ARKCLI_*_API_KEY` / `ARK_*_API_KEY`)**不是同一套**;5 条路由共用一组 AK/SK。这一组 AK/SK 能**同时**查到 Agent Plan 与 Coding Plan(含团队版席位)——不存在「Coding Plan 专用查询 key」 |
|
|
170
169
|
| SiliconFlow | `SILICONFLOW_API_KEY` | 余额型:`GET /v1/user/info` 返回账户余额,**与推理是同一把 Key** |
|
|
171
170
|
| OpenRouter | `OPENROUTER_API_KEY` | key 限额与用量用推理 key 即可;**账户余额需要 management / provisioning key**,普通 key 会被 403 拒——此时静默降级为只显示限额,不判失败 |
|
|
172
171
|
| Synthetic | `SYNTHETIC_API_KEY` | 模型订阅请求额度 |
|
|
@@ -180,7 +179,7 @@ $env:DEEPSEEK_API_KEY = "<你的 DeepSeek API Key>" # 启动环境;改完需
|
|
|
180
179
|
| ZenMux | `ZENMUX_MANAGEMENT_API_KEY` | 额度端点**只认 Management API Key**(推理 key 不适用),所以变量名单独区分。在 <https://zenmux.ai/platform/management> 创建该 key |
|
|
181
180
|
| LiteLLM | `LITELLM_API_KEY` + 代理地址 | 自建网关:除虚拟 Key(**与推理同一把**)还要在凭据区填写自己的 proxy 地址。管理端点在 **proxy 根**——填了 `/v1` 也会被去掉,不会拼成 `/v1/key/info` |
|
|
182
181
|
|
|
183
|
-
按 provider id 的字面映射(补齐上表按语义分组、未逐条列出的
|
|
182
|
+
按 provider id 的字面映射(补齐上表按语义分组、未逐条列出的 5 条火山路由,逐条对应 `PROVIDERS[id].envName`):
|
|
184
183
|
|
|
185
184
|
```text
|
|
186
185
|
deepseek DEEPSEEK_API_KEY
|
|
@@ -210,8 +209,6 @@ arkcli-agent-plan ARKCLI_AGENT_PLAN_API_KEY (额度用 VOLC_ACCESS
|
|
|
210
209
|
arkcli-coding-plan ARKCLI_CODING_PLAN_API_KEY
|
|
211
210
|
arkcli-agent-plan-team ARKCLI_AGENT_PLAN_TEAM_API_KEY
|
|
212
211
|
arkcli-coding-plan-team ARKCLI_CODING_PLAN_TEAM_API_KEY
|
|
213
|
-
ark-coding-plan-cn ARK_CODING_PLAN_CN_API_KEY
|
|
214
|
-
ark-agent-plan-cn ARK_AGENT_PLAN_CN_API_KEY
|
|
215
212
|
ark-coding-plan-byteplus ARK_CODING_PLAN_BYTEPLUS_API_KEY
|
|
216
213
|
```
|
|
217
214
|
|
|
@@ -251,7 +248,7 @@ ark-coding-plan-byteplus ARK_CODING_PLAN_BYTEPLUS_API_KEY
|
|
|
251
248
|
|
|
252
249
|
调用约定(**结果永远不为空**):
|
|
253
250
|
|
|
254
|
-
- 模型手里的名字往往不是这里的 provider id(它更可能看到 DSH 的路由名,例如 `zai`),所以匹配是**宽松**的:一个名字命中多条路由时**全部返回**(`zai` → 中国版 + 国际版,`ark` →
|
|
251
|
+
- 模型手里的名字往往不是这里的 provider id(它更可能看到 DSH 的路由名,例如 `zai`),所以匹配是**宽松**的:一个名字命中多条路由时**全部返回**(`zai` → 中国版 + 国际版,`ark` → 5 条),每条各自带着真实 `state`(没启用的如实标 `disabled`),由调用方自己判断要看哪条。
|
|
255
252
|
- 一个名字都认不出时,返回里会**先**给一条说明(写明可用写法),**再附上全部 30 条数据**——数据先给出去,判断交给调用方,而不是回一个空结果。
|
|
256
253
|
|
|
257
254
|
**Host 服务**(Cordis `Service`,key 为 `subUsage`):
|
package/lib/client.js
CHANGED
|
@@ -279,9 +279,10 @@ window.__ModuleLoader__.load({
|
|
|
279
279
|
// 企业版/团队版:额度按席位下发,需 AK/SK 有查看席位的权限。
|
|
280
280
|
"arkcli-agent-plan-team": { label: "ARK Agent Plan Team (arkcli)", short: "ARK Plan Team", envName: "ARKCLI_AGENT_PLAN_TEAM_API_KEY", volc: true, defaultEnabled: false },
|
|
281
281
|
"arkcli-coding-plan-team": { label: "ARK Coding Plan Team (arkcli)", short: "ARK Code Team", envName: "ARKCLI_CODING_PLAN_TEAM_API_KEY", volc: true, defaultEnabled: false },
|
|
282
|
-
// 旧插件 @volcengine/ark-plan-api
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
// 旧插件 @volcengine/ark-plan-api 的国内两条路由已从 Host 移除(实测与 arkcli 查同一份订阅、
|
|
283
|
+
// 同一组 AK/SK)。它们的 id 不在本表里,由 ROUTE_PROVIDER_ALIAS 在入口归并到 arkcli 路由,
|
|
284
|
+
// 装了旧插件的用户选中那些模型的药丸照常显示。
|
|
285
|
+
// BytePlus 是唯一「同 Action 不同 host」的:独立站点、独立订阅,所以保留自己的条目。
|
|
285
286
|
"ark-coding-plan-byteplus": { label: "Ark Coding Plan (BytePlus, legacy plugin)", short: "Ark BytePlus", envName: "ARK_CODING_PLAN_BYTEPLUS_API_KEY", volc: true, defaultEnabled: false },
|
|
286
287
|
// 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
|
|
287
288
|
"siliconflow": { label: "SiliconFlow", short: "SiliconFlow", envName: "SILICONFLOW_API_KEY", defaultEnabled: false },
|
|
@@ -300,10 +301,18 @@ window.__ModuleLoader__.load({
|
|
|
300
301
|
"litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
|
|
301
302
|
};
|
|
302
303
|
// 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
|
|
303
|
-
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", "ark-coding-plan-
|
|
304
|
+
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", "ark-coding-plan-byteplus", "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"];
|
|
304
305
|
// 火山方舟的 7 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
|
|
305
306
|
// 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
|
|
306
307
|
const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
|
|
308
|
+
/**
|
|
309
|
+
* 旧插件 @volcengine/ark-plan-api 注册的两个国内路由 id **已从本插件移除**:它们与 arkcli 的路由
|
|
310
|
+
* 查的是同一份订阅(同 Action、同 host、同 AK/SK,实测请求 URL 逐字相同),并列显示只会让人以为
|
|
311
|
+
* 买了两个套餐,数字还会因为各自缓存而差出几十秒的量。
|
|
312
|
+
* 但装了旧插件的用户,模型路由仍然是这两个 id —— 药丸必须在入口把它们归并到 arkcli 路由,
|
|
313
|
+
* 否则那些用户会突然失去用量显示。BytePlus 没有别名:它是独立站点、独立订阅。
|
|
314
|
+
*/
|
|
315
|
+
const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan" };
|
|
307
316
|
function providerEnabled(settings, id) {
|
|
308
317
|
if (PROVIDER_META[id]?.volc) {
|
|
309
318
|
// 组开关优先;旧配置里没有它时按「组内任一条开着」合并过来,全新配置则沿用默认。
|
|
@@ -313,14 +322,21 @@ window.__ModuleLoader__.load({
|
|
|
313
322
|
return settings?.visibility?.providers?.[id] ?? (PROVIDER_META[id]?.defaultEnabled !== false);
|
|
314
323
|
}
|
|
315
324
|
// provider id 就是路由 id,而路由由提供方插件/CLI 按「你实际订阅了什么」写入:例如 arkcli helper
|
|
316
|
-
//
|
|
317
|
-
//
|
|
325
|
+
// 只会为真正持有的套餐写路由。对**按路由取数的**提供商来说,「本机没有这条路由」就等于「没订阅」,
|
|
326
|
+
// 默认收起比「常量里默认开启」更贴近事实。undefined = 拿不到路由表,按「未知」放行,不谎报未安装。
|
|
327
|
+
// 例外是火山方舟:它的额度走账号级管控面(一组 AK/SK 覆盖名下所有套餐),路由只是推理入口 —— 见 providerVisible。
|
|
318
328
|
function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
|
|
319
329
|
function providerVisible(settings, id, entry, result) {
|
|
320
330
|
if (!providerEnabled(settings, id)) return false;
|
|
321
|
-
// 关掉「没有检测到 API
|
|
331
|
+
// 关掉「没有检测到 API 的默认隐藏」就是要看全部,不再按路由、凭据或订阅状态收起。
|
|
322
332
|
if (settings?.visibility?.hideWithoutApi === false) return true;
|
|
323
|
-
|
|
333
|
+
// 火山方舟**不按推理路由收起**:额度是账号级订阅,路由装没装与额度无关——一组 IAM AK/SK 就能
|
|
334
|
+
// 查到名下所有套餐(GetCodingPlanUsage 与 GetAFPUsage 同协议同凭据)。但它**要按接口回报的
|
|
335
|
+
// 订阅状态收起**:7 条路由一律列出来,用户根本分不清哪个是哪个,而其中多数是「未检测到该套餐订阅」。
|
|
336
|
+
// `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
|
|
337
|
+
if (PROVIDER_META[id]?.volc) {
|
|
338
|
+
if (entry?.subscribed === false) return false;
|
|
339
|
+
} else if (!providerRoutable(result, id)) return false;
|
|
324
340
|
return entry?.apiDetected !== false && !["no-key", "no-cookie"].includes(entry?.state);
|
|
325
341
|
}
|
|
326
342
|
/** token 数紧凑格式:1,234,567,890 → 1.23B */
|
|
@@ -1556,8 +1572,12 @@ window.__ModuleLoader__.load({
|
|
|
1556
1572
|
function SubusagePillEntry({ store, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
|
|
1557
1573
|
const state = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1558
1574
|
const provider = state.current && state.current.provider;
|
|
1559
|
-
|
|
1560
|
-
|
|
1575
|
+
// 旧插件 @volcengine/ark-plan-api 的两个国内路由 id 不在本表里,但它们是**同一个 plan**:
|
|
1576
|
+
// 不管模型路由由哪个插件提供,都归并到 arkcli 路由再查——所以只会有**一个**面板,
|
|
1577
|
+
// 数据来自同一份订阅(实测两条路由的请求 URL 逐字相同)。BytePlus 没有别名,它是独立站点。
|
|
1578
|
+
const resolved = ROUTE_PROVIDER_ALIAS[provider] ?? provider;
|
|
1579
|
+
const info = PROVIDER_ORDER.includes(resolved) ? PROVIDER_META[resolved] : null;
|
|
1580
|
+
const meta = info ? { label: resolved === "commandcode" ? info.label : info.short, id: resolved } : null;
|
|
1561
1581
|
// 所有 Hook 必须先于条件返回:模型切换或重连时 provider 可能暂时为空。
|
|
1562
1582
|
// 稳定引用:否则父组件每次渲染都新建函数,药丸 effect 会反复重启轮询。
|
|
1563
1583
|
const providerId = meta ? meta.id : null;
|
package/lib/index.js
CHANGED
|
@@ -54,11 +54,11 @@ const PROVIDERS = {
|
|
|
54
54
|
// 企业版/团队版:额度挂在席位上,先 ListSeatInfos 取 SeatID 再查该席位(volcSeat 决定用哪对 Action)。
|
|
55
55
|
"arkcli-agent-plan-team": { label: "ARK Agent Plan Team (arkcli)", envName: "ARKCLI_AGENT_PLAN_TEAM_API_KEY", credentialKind: "volc", volcSeat: "agent", volcScene: "agent_plan_enterprise", volcHost: "open.volcengineapi.com", volcRegion: "cn-beijing", defaultEnabled: false },
|
|
56
56
|
"arkcli-coding-plan-team": { label: "ARK Coding Plan Team (arkcli)", envName: "ARKCLI_CODING_PLAN_TEAM_API_KEY", credentialKind: "volc", volcSeat: "coding", volcScene: "coding_plan_enterprise", volcHost: "open.volcengineapi.com", volcRegion: "cn-beijing", defaultEnabled: false },
|
|
57
|
-
// ── 旧插件 @volcengine/ark-plan-api
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// BytePlus
|
|
57
|
+
// ── 旧插件 @volcengine/ark-plan-api 的路由 ───────────────────────────────
|
|
58
|
+
// 它的国内两条(`ark-coding-plan-cn` / `ark-agent-plan-cn`)**已移除**:实测与 arkcli 的路由请求 URL
|
|
59
|
+
// 逐字相同(同 Action、同 host、同 AK/SK、同一份订阅),并列显示只会让人以为买了两个套餐,
|
|
60
|
+
// 数据还会因为各自缓存而差出几十秒的量。药丸仍认这两个 id —— Client 的路由别名把它们归并到 arkcli 路由。
|
|
61
|
+
// BytePlus 保留:这是唯一的「同 Action 不同 host」,走 ap-southeast-1 独立站点与独立订阅,删了没人覆盖。
|
|
62
62
|
"ark-coding-plan-byteplus": { label: "Ark Coding Plan (BytePlus, legacy plugin)", envName: "ARK_CODING_PLAN_BYTEPLUS_API_KEY", credentialKind: "volc", volcAction: "GetCodingPlanUsage", volcHost: "ark.ap-southeast-1.byteplusapi.com", volcRegion: "ap-southeast-1", defaultEnabled: false },
|
|
63
63
|
// DeepSeek 官方:余额型(不是订阅窗口),与推理**同一把** API Key,端点与响应见官方文档
|
|
64
64
|
// https://api-docs.deepseek.com/zh-cn/api/get-user-balance
|
|
@@ -85,6 +85,16 @@ const IDS = Object.keys(PROVIDERS);
|
|
|
85
85
|
// 用一个组级开关控制整组。这些 id 仍各自存在——药丸要靠 provider id 匹配路由,不能合并。
|
|
86
86
|
const ARK_IDS = IDS.filter(id => PROVIDERS[id].credentialKind === "volc");
|
|
87
87
|
const ARK_DEFAULT_ON = ARK_IDS.some(id => PROVIDERS[id].defaultEnabled !== false);
|
|
88
|
+
/**
|
|
89
|
+
* 某条路由当前是否启用。Ark 以**组开关为唯一真源**——7 条路由共用一组 IAM AK/SK、一般用户只
|
|
90
|
+
* 持有一个套餐,所以设置页只给一个开关。这里必须与 Client 的 `providerEnabled` 同语义:
|
|
91
|
+
* 两端口径一旦分叉,界面按组开关显示这条路由,Host 却按逐条值回 `disabled`,
|
|
92
|
+
* 用户拿到的就是一张写着「检测已关闭」的空卡片(这正是 Coding Plan 查不到用量的原因)。
|
|
93
|
+
*/
|
|
94
|
+
function providerEnabled(settings, id) {
|
|
95
|
+
if (PROVIDERS[id]?.credentialKind === "volc" && typeof settings?.visibility?.ark === "boolean") return settings.visibility.ark;
|
|
96
|
+
return !!settings?.visibility?.providers?.[id];
|
|
97
|
+
}
|
|
88
98
|
/**
|
|
89
99
|
* 调用方(尤其是模型)手里的「提供商名」通常**不是**本插件的 provider id。模型能看到的往往是
|
|
90
100
|
* DSH 的 llm 路由名或厂商品牌名(`zai`、`grok`、`codex`、`ark`),而这里的 id 是路由 id
|
|
@@ -199,6 +209,16 @@ function bearerSubject(key) {
|
|
|
199
209
|
// ({ version, credential: { access, accountId } })。槽位优先 auth.x.ai,其次旧签发方。
|
|
200
210
|
function grokAuthFileCredentials(io) {
|
|
201
211
|
const str = v => typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
212
|
+
// access token 短命(实测 ~24 小时量级)。登录文件里带着 `expires_at`,必须把它读出来:
|
|
213
|
+
// 不读就只能等上游回 401,界面显示一句「登录或凭据已失效」,而用户那边的 Grok Kit 明明写着「已登录」。
|
|
214
|
+
const expiry = value => {
|
|
215
|
+
if (typeof value === "string" && Number.isFinite(Date.parse(value))) return new Date(value).toISOString();
|
|
216
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
217
|
+
const ms = value > 1e12 ? value : value * 1000;
|
|
218
|
+
return Number.isFinite(new Date(ms).getTime()) ? new Date(ms).toISOString() : undefined;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
};
|
|
202
222
|
const read = path => {
|
|
203
223
|
try {
|
|
204
224
|
const parsed = JSON.parse(io.readFileSync(path, "utf8"));
|
|
@@ -206,7 +226,9 @@ function grokAuthFileCredentials(io) {
|
|
|
206
226
|
const envelope = parsed.credential && typeof parsed.credential === "object" && !Array.isArray(parsed.credential) ? parsed.credential : undefined;
|
|
207
227
|
const pick = (record, userId) => {
|
|
208
228
|
const key = str(record.key) ?? str(record.access) ?? str(record.access_token);
|
|
209
|
-
|
|
229
|
+
if (!key) return undefined;
|
|
230
|
+
const expiresAt = expiry(record.expires_at ?? record.expiresAt);
|
|
231
|
+
return { key, userId: str(record.user_id) ?? str(record.principal_id) ?? userId ?? bearerSubject(key), ...(expiresAt ? { expiresAt } : {}) };
|
|
210
232
|
};
|
|
211
233
|
if (envelope) return pick(envelope, str(envelope.accountId));
|
|
212
234
|
const slots = Object.entries(parsed).filter(([, v]) => v && typeof v === "object" && !Array.isArray(v) && (str(v.key) ?? str(v.access) ?? str(v.access_token)));
|
|
@@ -251,7 +273,9 @@ function redactMimo(value, cookie) {
|
|
|
251
273
|
return { ...value, ...(Array.isArray(value.extras) ? { extras: value.extras.map(extra => extra.kind === "plan" ? { ...extra, value: sanitize(extra.value) } : extra) } : {}) };
|
|
252
274
|
}
|
|
253
275
|
function defaultSettings() {
|
|
254
|
-
|
|
276
|
+
// Ark 的逐条默认值必须直接跟随组开关:`load()` 在配置文件还不存在时走的就是这里,若默认值各行其是,
|
|
277
|
+
// 「首次读到 → 保存 → 再读回」会因为归一化把成员值改成组开关的值而算出不同的 revision。
|
|
278
|
+
return { zai: { type: 1, organization: "", project: "" }, volc: { accessKeyId: "", secretAccessKey: "" }, litellm: { baseUrl: "" }, xiaomi: { cookie: "", loginAt: 0, expiresAt: 0 }, keys: {}, keyModes: Object.fromEntries(IDS.map(id => [id, "inherit"])), visibility: { hideWithoutApi: true, ark: ARK_DEFAULT_ON, providers: Object.fromEntries(IDS.map(id => [id, PROVIDERS[id].credentialKind === "volc" ? ARK_DEFAULT_ON : PROVIDERS[id].defaultEnabled !== false])) } };
|
|
255
279
|
}
|
|
256
280
|
function parseStored(value) {
|
|
257
281
|
const s = defaultSettings();
|
|
@@ -270,6 +294,10 @@ function parseStored(value) {
|
|
|
270
294
|
if (typeof value.keys?.[id] === "string") s.keys[id] = value.keys[id];
|
|
271
295
|
if (value.keyModes?.[id] === "manual") s.keyModes[id] = "manual";
|
|
272
296
|
}
|
|
297
|
+
// 组开关是 Ark 的**唯一真源**:逐条值只当作迁移输入,读回后一律跟随组开关。
|
|
298
|
+
// 不归一化就会分叉——「迁移时把任一条开着合并成组开关 true、成员值却保留原样」之后,
|
|
299
|
+
// 界面按组开关显示这条路由,Host 却按成员值判定,用户拿到的是一张写着「检测已关闭」的空卡片。
|
|
300
|
+
for (const id of ARK_IDS) s.visibility.providers[id] = s.visibility.ark;
|
|
273
301
|
return s;
|
|
274
302
|
}
|
|
275
303
|
function publicSettings(s) {
|
|
@@ -371,7 +399,7 @@ function volcFailure(action, error) {
|
|
|
371
399
|
}
|
|
372
400
|
async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
373
401
|
let response;
|
|
374
|
-
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.
|
|
402
|
+
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.7", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
375
403
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
376
404
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
377
405
|
if (!response.ok) {
|
|
@@ -887,9 +915,10 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
887
915
|
if (PROVIDERS[id].credentialKind === "volc") return await this.volcIdentity(id, s);
|
|
888
916
|
if (id === "xai-oauth") {
|
|
889
917
|
// SuperGrok 只认共享登录文件里的 OAuth Bearer:token 短命轮换,凭据服务/环境变量副本
|
|
890
|
-
// 必然过期顶掉新 token,故不走通用 Key 链;user_id 随行给 x-userid
|
|
918
|
+
// 必然过期顶掉新 token,故不走通用 Key 链;user_id 随行给 x-userid 请求头,
|
|
919
|
+
// expiresAt 用来在过期时给出「去刷一次」而不是「凭据已失效」。
|
|
891
920
|
const cred = await this.grokAuth();
|
|
892
|
-
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.userId ? { userId: cred.userId } : {}) } : { key: undefined, source: "none" };
|
|
921
|
+
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.userId ? { userId: cred.userId } : {}), ...(cred.expiresAt ? { expiresAt: cred.expiresAt } : {}) } : { key: undefined, source: "none" };
|
|
893
922
|
}
|
|
894
923
|
if (id === "openai-codex") {
|
|
895
924
|
// 同上:只读 Codex CLI 的 OAuth 登录文件,token 由 Codex CLI 轮换,第三端刷新会互相顶掉。
|
|
@@ -939,6 +968,10 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
939
968
|
const signed = volcSignature({ accessKeyId: identity.key, secretAccessKey: identity.secret, action, host: spec.volcHost, region: spec.volcRegion, body });
|
|
940
969
|
const response = await get(signed.url, signed.headers, { method: "POST", body });
|
|
941
970
|
const apiError = volcError(response);
|
|
971
|
+
// 「这个账号没有这份套餐」不是故障,是答案:实测没有 BytePlus Coding Plan 的账号会返回
|
|
972
|
+
// HTTP 404 `NotFound.BillingType`(coding plan config is not exist),`OperationDenied.NotSubscribed`
|
|
973
|
+
// 同义。把它当错误抛出去,界面上就多一张红色卡片;正确做法是回报 subscribed:false 让前端收起。
|
|
974
|
+
if (apiError && /^(NotFound\.BillingType|OperationDenied\.NotSubscribed)$/.test(apiError.code)) return { notSubscribed: true };
|
|
942
975
|
if (apiError) throw volcFailure(action, apiError);
|
|
943
976
|
return response;
|
|
944
977
|
};
|
|
@@ -948,7 +981,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
948
981
|
// 企业版/团队版:额度挂在席位上,先取 SeatID 再查该席位。
|
|
949
982
|
if (spec.volcSeat) {
|
|
950
983
|
const seatIds = parseVolcSeatIds(await call("ListSeatInfos", { Filter: {}, Scene: spec.volcScene, PageNum: 1, PageSize: 100 }));
|
|
951
|
-
|
|
984
|
+
// 没有可读席位就是「这条团队版订阅用不上」——对用户而言与未订阅同义,如实回报以便前端收起。
|
|
985
|
+
if (!seatIds.length) return { windows: [], extras: [{ kind: "plan", value: "该账号下没有可读的席位(或当前身份无权查看)" }], coverage: "partial", subscribed: false };
|
|
952
986
|
const seatId = seatIds[0];
|
|
953
987
|
// 多个席位时只看第一个,但如实说明总数,不假装看全了。
|
|
954
988
|
const seatNote = seatIds.length > 1 ? `${seatId}(共 ${seatIds.length} 个席位)` : seatId;
|
|
@@ -964,9 +998,10 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
964
998
|
if (parsed.plan) extras.push({ kind: "plan", value: parsed.plan });
|
|
965
999
|
// 与个人版口径一致:没有窗口时说清「本周期没有可读额度」,而不是只给一个 partial 状态。
|
|
966
1000
|
extras.push({ kind: "account", value: parsed.windows.length ? `席位 ${seatNote}` : `席位 ${seatNote}(本周期没有可读的额度窗口)` });
|
|
967
|
-
return { windows: toWindows(parsed), extras, coverage: parsed.windows.length ? "complete" : "partial" };
|
|
1001
|
+
return { windows: toWindows(parsed), extras, coverage: parsed.windows.length ? "complete" : "partial", subscribed: parsed.subscribed === true };
|
|
968
1002
|
}
|
|
969
1003
|
const body = await call(spec.volcAction);
|
|
1004
|
+
if (body?.notSubscribed) return { windows: [], extras: [{ kind: "plan", value: "该账号没有订阅这个套餐" }], coverage: "complete", subscribed: false };
|
|
970
1005
|
let parsed;
|
|
971
1006
|
try { parsed = spec.volcAction === "GetAFPUsage" ? parseVolcAgentPlan(body) : parseVolcCodingPlan(body); }
|
|
972
1007
|
catch { throw failure("subusage/response", `火山方舟 ${spec.volcAction} 的额度结构无法解析`); }
|
|
@@ -975,7 +1010,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
975
1010
|
if (parsed.plan) extras.push({ kind: "plan", value: parsed.plan });
|
|
976
1011
|
// HTTP 200 但窗口为空是「未订阅」,不是 0% 用量:显式说明,不画零额度的假象。
|
|
977
1012
|
else if (!windows.length) extras.push({ kind: "plan", value: parsed.subscribed ? "本周期暂无窗口数据" : "未检测到该套餐订阅" });
|
|
978
|
-
|
|
1013
|
+
// 订阅状态要**结构化**带出去,不能只写进 extras 文案:同一组 AK/SK 能查到名下所有套餐,
|
|
1014
|
+
// 界面上若把 7 条路由一律列出来,用户根本分不清哪个是哪个。前端据此收起没订阅的那些。
|
|
1015
|
+
return { windows, extras, coverage: windows.length ? "complete" : "partial", subscribed: parsed.subscribed === true };
|
|
979
1016
|
}
|
|
980
1017
|
const headers = { authorization: `Bearer ${identity.key}` };
|
|
981
1018
|
if (id === "synthetic") return normalizeSynthetic(await get("https://api.synthetic.new/v2/quotas", headers));
|
|
@@ -1047,6 +1084,13 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1047
1084
|
return normalizeCommandCode(credits, subscription);
|
|
1048
1085
|
}
|
|
1049
1086
|
if (id === "xai-oauth") {
|
|
1087
|
+
// 登录文件里的 access token 短命(实测 ~24 小时)。本插件**刻意不代刷**——refresh-token 轮换由
|
|
1088
|
+
// Grok CLI / dsh-grok-kit 各自的锁协议管理,第三端刷新会把它们的轮换顶掉。但「过期」这件事必须
|
|
1089
|
+
// 说清楚:不发这一枪的话,用户看到的是上游 401 转成的「登录或凭据已失效」,而他那边的 Grok Kit
|
|
1090
|
+
// 明明写着「已登录」——两边对不上,只会让人以为插件坏了。
|
|
1091
|
+
if (identity.expiresAt && Date.parse(identity.expiresAt) <= this.now()) {
|
|
1092
|
+
throw failure("subusage/auth", `Grok 登录文件的 access token 已于 ${new Date(identity.expiresAt).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} 过期。用一次 Grok CLI,或在 设置 → xAI Grok 里重新登录,都会刷新同一个文件(${GROK_AUTH_FILE});本插件不代刷,以免与它们的 refresh-token 轮换互相顶掉。`);
|
|
1093
|
+
}
|
|
1050
1094
|
// 直连官方 Grok CLI 计费代理;X-XAI-Token-Auth 的取值是 token_header 字符串(xai-grok-cli)
|
|
1051
1095
|
// 而非布尔,x-userid 取登录文件。套餐名走 /v1/settings(subscription_tier_display),
|
|
1052
1096
|
// 失败不阻塞用量(回退 billing 响应里的 subscription_tier)。
|
|
@@ -1084,7 +1128,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1084
1128
|
catch { this.cache.delete(id); return { ...this.base(id, "none", settings, account), state: "error", errorCode: "subusage/credentials", error: "无法解析凭据" }; }
|
|
1085
1129
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1086
1130
|
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
1087
|
-
if (!settings
|
|
1131
|
+
if (!providerEnabled(settings, id)) {
|
|
1088
1132
|
this.cache.delete(id); this.inflight.delete(id);
|
|
1089
1133
|
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
1090
1134
|
}
|
package/lib/volcengine.js
CHANGED
|
@@ -110,7 +110,10 @@ const CODING_LEVELS = {
|
|
|
110
110
|
/**
|
|
111
111
|
* 个人版 Coding Plan:Result.QuotaUsage[] 每项一个窗口。
|
|
112
112
|
* Percent 是**已用百分数**(0–100,实测常见 0.39 这类小值,不能按「≤1 即小数」放大)。
|
|
113
|
-
* 接口只给百分比,不返回绝对量,因此不产出 detail
|
|
113
|
+
* 接口只给百分比,不返回绝对量,因此不产出 detail。
|
|
114
|
+
* 真实响应里还带 `Cap` / `RewardTotalPercent` / `HasReward`(2026-10-10 用本机 AK/SK 实测:三个窗口
|
|
115
|
+
* 都是 `Cap: 100`、`RewardTotalPercent: 0`、`HasReward: false`)——**字段确实存在**,但没有任何官方
|
|
116
|
+
* 说明它们的语义(像百分比刻度,也像次数上限),所以不展示:不编造单位,也不把 100 读成"还有 100 次"。
|
|
114
117
|
*/
|
|
115
118
|
export function parseVolcCodingPlan(body) {
|
|
116
119
|
// 正常走 OpenAPI 的 ResponseMetadata + Result 信封;个别抓包形态把字段放在顶层,两者都接受。
|