# dsh-token-plan-quota

[中文](README.md) | [English](README.en.md)

**在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的输入框工具行里，显示「你正在用的那家还剩多少额度」。**

做它的起因很具体：**阿里云 / 千问 Token Plan 有额度，却没有公开的额度接口**——只能走控制台数据网关
（Cookie 会话），所以生态里的同类插件普遍不碰它。本插件把那套契约内置了，顺手也把有官方接口的几家
（DeepSeek、Moonshot、OpenRouter、阿里云费用中心）按真值接上。

三条口径，一眼看完：

| 口径 | 表现 |
|---|---|
| **有官方接口 → 真值** | 有分母就画余量条、报百分比；没分母就只报数——DeepSeek 余额没有"总量"这个分母，所以不画条 |
| **没有官方接口 → 标「实测」** | 只报**经过本实例**的 token / 次数。不折算 Credits、不按历史推算剩余、**永不画条、永不报百分比** |
| **零配置** | 装完按你实际在用的供应商路由决定开哪些源；不用写 `sources`，也不用手填 `providers` |

```
输入框工具行里的徽标是个小胶囊，跟随当前模型的供应商切换（名字/速度/天数都在 tooltip 里）：

  ▓▓ 75%                  官方给了分母 → 画条、报百分比
  123.5万 tok  实测        没有官方分母 → 只报用了多少，绝不画条、绝不报百分比
```

**① 徽标跟随当前模型的供应商切换**——这一家官方接口给真值，切到没有官方额度接口的那家就退回「实测」：

![徽标跟随模型在两家供应商之间切换](docs/images/badge-follows-model.gif)

**② 明细面板**：官方余量卡（有官方分母才画条、才给百分比）+ 一张卡里的多窗口计量条：

![明细面板：官方余量卡与多窗口计量条](docs/images/panel-official-plus-meters.png)

**③ 面板可以拖成悬浮小窗**（拖标题栏，双击复位）：

![面板拖成悬浮小窗](docs/images/floating-panel.png)

**④ Cookie 掉线时退回「实测」卡**：官方卡变成一张写清原因的**错误卡、不带任何数字**，兜底的实测卡留在徽标上——
没有官方分母就不画条、不报百分比：

![Cookie 掉线时退回实测卡](docs/images/cookie-fallback-measured.png)

**⑤ 按钱计费的模型也认得**：DeepSeek 的余额是官方真值，但它没有"总量"这个分母 ⇒ 不画条、不报百分比，
只把「还剩多少钱、充过多少、赠过多少」原样报出来：

![按钱计费的官方余额卡：没有分母就不画条](docs/images/state-deepseek-balance.png)

**⑥ 用过的模型会显示额度**：官方接口真给了分母的，才画余量条、才报百分比和周期重置日：

![Token Plan 官方余量卡：有分母才画条](docs/images/state-token-plan-credits.png)

**⑦ 切到本实例没用过的模型时不会显示额度**：这一家没有官方额度接口，而实测账目里还没有它的调用记录
—— 于是徽标写「无官方额度数据」，面板只说清为什么没有，**不会**顺手编一个数：

![没有官方额度、也没有调用记录时的实测卡](docs/images/state-no-history.png)

> 上面七张图里的余额、token 数、日期**全部是合成示例**，不是任何真实账号的额度（本插件不会把真实数字放进公开仓库）。
> 英文套只有六张：⑦ 那句解释文案宿主目前只有中文（见 [ROADMAP](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/ROADMAP.md)），与其给它编一句英文，不如少一张图。
> 想自己复现或换图：[scripts/shots/README.md](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/scripts/shots/README.md)。

## 它不做什么

这是本插件的立场，比功能列表更重要：

- **不折算 Credits**，不做抵扣率，不"按历史推算剩余"；
- **没有"我自己填一个额度上限"的窗口**：分母只能来自官方接口，手填的上限就是编数，所以本插件没有任何
  表达"周期 + 上限"的键。`token-plan-window` **不是**自建窗口预设，它是千问 Token Plan 的本地实测账本
  （不打网络，只记经过本实例的调用）；
- **没有官方分母就不派生百分比**：`used/total`、`100−已用%` 这类算出来的比例一律不出；实测卡永不画余量条、
  永不显示百分比，只报「窗口内用了多少 token / 多少次」。但**上游直接返回的比例属官方真值**——千问控制台
  的 `usage.per1MonthPercentage` 就是它给的数，所以档位额度那一路偶发取不到时（实测会超时），徽标仍显示
  「96% + 条」（条画的就是那个官方比例，不是第二个事实），只是**不报绝对剩余量**（算不出来的东西不显示），并留 `extra.denominatorFailed` 诊断；
- **档位配置不等于你的额度**：一个窗口只有在这个套餐真回了读数时才存在（上游 `quota-config` 里躺着的
  `five_hour` 上限不代表这个账号有 5 小时窗口）；
- **认不准就不开**：自动检测宁可少一张卡，也不会把别家的余额数字顶在你正在用的模型上；
- **不做会产生计费的探活**；只用只读端点；
- **不读别的 CLI 的登录态**（见[明确不做的三类](#明确不做的三类)）。

## 安装

```bash
dsh plugin --profile web add dsh-token-plan-quota          # npm 包名
dsh plugin --profile web add github:xinghe-1018/dsh-token-plan-quota
dsh plugin --profile web add ./dsh-token-plan-quota        # 本地目录
```

`dsh plugin` 是 pnpm 直传到 profile 目录；本插件**零第三方依赖、无构建步骤**，所以不会撞上 pnpm ≥10 的
`prepare` 脚本白名单。装完**重启一次 `dsh web`**（bundle 与客户端入口在启动时组装）。

运行时唯一的外部资源是三枚 CDN 字体 link，加载失败自动落系统字体栈，不影响功能。

## 支持哪些厂家

| 厂家 | 额度来源 | 徽标上看到什么 | 验证到什么程度 |
|---|---|---|---|
| DeepSeek | ✅ 官方 `/user/balance`（Bearer） | 余额 + 充值 + 赠款（真值） | 真实 Key 实测 |
| 千问 Token Plan | ⚠️ 控制台数据网关（**Cookie 会话**，非公开 API） | 已用 % + 进度条（与订阅页同源）；Cookie 没配/过期 → 退回实测卡 | 真账号实测 |
| Moonshot / Kimi 开放平台 | ✅ 官方 `/v1/users/me/balance`（Bearer，`.cn` CNY / `.ai` USD 两区） | 余额 + 充值 + 赠款（真值） | 端点存在性实测；**字段名未用真 Key 核对** |
| OpenRouter | ✅ 官方 `/api/v1/credits`（Bearer） | 余额（USD）＝累计充值 − 累计花费 | 端点存在性实测；**字段名未用真 Key 核对** |
| 阿里云费用中心 | ✅ BssOpenApi（AK/SK 签名） | 账户余额 / 资源包余量（真值） | 逐字段对过官方 OpenAPI 元数据 |
| MiniMax、普通 Key 的 OpenAI / Gemini 等 | ❌ 官方无 Key 化额度端点 | **实测**卡（只报 token/次数） | 常见路径实测过，均无端点 |

逐条端点、信封字段、单位与百分比方向陷阱见 [`docs/upstream-contracts.md`](docs/upstream-contracts.md)。

**只看 API host，不看控制台域名**：`platform.kimi.com`、`platform.moonshot.cn`、
`bailian.console.aliyun.com` 这类**网页控制台**域名一律不参与识别——插件查的是余额接口，落在路由
`baseURL` 指向的 API host（`api.moonshot.cn` / `api.moonshot.ai`）上。Kimi 开放平台的 Key 与 Moonshot
开放平台是同一套账号与余额体系，所以 Key 配好后要走 `api.moonshot.*`；若你的 `baseURL` 写成别的域名，
检测认不出，会退回实测卡。想强制开官方卡，别只写源名——**要把它绑到你真正的路由 id 上**，否则默认
`panelScope: "current"` 下这张卡查得到却不露面（预设自带的 `providers` 是 `moonshot` 这类通用名，不含你的路由名）：

```json
{ "sources": [{ "id": "moonshot-balance", "providers": ["kimi-open-cn"] }] }
```

（下表最后一列请注意：这一家的字段名还没用真 Key 核对过，强制开出来若数字不对，用 `probe` 对字段名。
`providers` 落空时宿主日志会直接提醒，不至于一边是空白面板、一边让人瞎猜。）

### 与相邻插件的区别

生态里已有做同类事的插件，主要是**成本统计 / 费用估算**方向（例如 `dsh-cost-meter`、`dsh-token-monitor`）。
本插件不抢那块，它的口径是**只报真值或明确标注的实测，不折算、不估算**，徽标**跟随当前模型的供应商**切换，
并且内置了**阿里云 / 千问 Token Plan 的控制台余量契约**——那一家有额度却没有公开接口，正是本插件当初被做
出来的原因。要费用报表请选前者；要"我正在用的这家还剩多少"，选这个。

## 结构总览

![架构示意图：浏览器半边、DSH 宿主进程、上游供应商三块怎么分工](docs/images/architecture.png)

一张图看完半边怎么分工：**浏览器半边**（`lib/client.js`）在输入框工具行渲染跟随当前模型的徽标，
点开是明细面板；**宿主半边**（`lib/index.js`）挂 `llm/stream` 记本实例账本、注册只读路由与
`token_plan_quota` 工具、按零配置检测出的在用路由去查上游；**上游**按有没有官方接口分两类——
有官方接口的报真值（DeepSeek / Moonshot / OpenRouter / 阿里云费用中心），千问 Token Plan 走控制台
数据网关（Cookie 会话）。凭据只进不出：Bearer / AK-SK / Cookie 只在宿主进程内解析，
只读路由与模型工具永不回传。

这张图不是手绘的：源规格在仓库的 `diagrams/` 里，再生成方式与前置条件见
[`diagrams/README.md`](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/diagrams/README.md)
（该目录不随 npm 包发布）。

## 零配置自动检测

宿主在用的供应商路由就是**唯一事实来源**，所以你不用写 `sources`，也不用手填 `providers`——历史上正是这两个
字段各写各的，导致"卡查得到却永远看不见"。识别按可信度排序，命中即停：

| 依据 | 说明 |
|---|---|
| `baseURL` 的 host | 最可信：路由实际打到哪。`api.moonshot.cn` → Moonshot 大陆区，`openrouter.ai` → OpenRouter，Token Plan 网关 → 千问 |
| 路由 id / 名称关键词 | **只在拿不到 host 时**降级使用（出厂的 `deepseek-official` 就靠这条） |
| Key 前缀 | 最后的线索（`sk-or-` → OpenRouter） |
| 都没命中 | 给**每条**认不出的路由各挂一张 `window:<provider>` 实测源：徽标不空，只报本地 token/次数 |

**本文里 `provider`、`路由 id`、`providers` 数组里的字符串是同一个东西**：宿主 `llm.listProviders()` 返回的
`id`（比较时忽略大小写，`.` `_` `/` 一律当 `-`）。所以 `window:minimax-cn` 的实测卡只在路由 id 为
`minimax-cn` 的模型下出现，检测自动挂的兜底卡也叫这个名字（`window:minimax-cn`）——两个写法指同一张卡。

两条不变量：

- **你写过的永远赢，但它是一层叠加，不是替换**。`autoDetect: true`（默认）时，你写的 `sources` 先落地，
  检测再补齐**你没管的部分**——不会因为多写了一条就把它不认识的卡全清掉。"已覆盖"只有三种精确含义：
  ① 你某条源的 `providers` 里含这个路由 id → 该路由不再追加任何源；② 你有一条同名 `id` 的源 → 检测不再开
  同名源（`skipped` 记 `id-taken-by-configured`）；③ 你写了 `{"id":"<源名>","enabled":false}` → 这个名字
  永不再自动开（`skipped` 记 `disabled-by-config`）。此外还有第四种可能：凭据解析不到的源整个不开
  （`skipped` 记 `no-credential`，并列出试过哪些引用名；不挂错误卡占地方）。
  `autoDetect: false` 才是"以你写的为唯一事实"。
- **可解释**。每张自动开出的卡带 `detected: {by, rule, host, fallback}`，标题 tooltip 写
  「按 api.moonshot.ai 自动识别 · 区 international」；快照的 `detection` 块交代在用路由、开了什么、
  谁因何被跳过、谁没被认出——排查"这家怎么不显示"只看这一处（**它只在
  `GET /token-plan-quota/summary` 的返回里，明细面板不显示这一坨**）。老宿主没有 `llm` 服务时静默退回配置语义。

## 界面

- **徽标跟随模型**：订阅宿主现成的 `sessions.list` → `modelDirectories` 运行时 store，模型一切换立刻改卡，
  不轮询、不打网络。优先级：有数字的官方卡 > 有数字的实测卡 > 报错的官方卡（提示去配 Cookie）。
- **一家只展示一张额度卡**：官方卡有数字时收起自动挂的实测兜底卡；官方卡变错误卡时兜底必须回来。
  收合发生在看得见数据的显示层，不在规划期——否则 Cookie 一过期徽标就空白。
- **一张卡可以有多个窗口**：宿主按主次排进 `card.meters`（**这是快照里的卡形状，不是手写条目的配置键**），
  `meters[0]` 与顶层数值同源、由标题行与大条表达，
  其余窗口各出一行「标签 · 剩余/总额 · 已用% · 重置日」带自己的细渐变条。
- **明细是常驻小窗**：可拖（按住标题栏）、可缩（右下角握柄）、位置与尺寸记进 `localStorage`、双击标题栏归位。
  **点哪儿都不会关掉它**——关闭只由标题栏那颗 `✕`、再点徽标或 `Esc` 决定。不做「点外面就关」：那是下拉菜单的语义，
  而且靠选择器识别宿主输入区必然漏（线上产物里连 `data-composer-card` 都不存在）。
- 视觉：幽灵小控件，无边框、无状态圆点、无表情符号，**余量渐变条即状态**（≥70% 绿、40–70% 蓝、<40% 橙→红）；
  「官方／实测」药丸标档级；字体自成一套（Geist Variable + Noto Sans SC，断网落系统栈），等宽只留给标识串；
  所有动效尊重 `prefers-reduced-motion`。
- `panelScope: current`（默认）只列当前模型供应商的卡；想全看设 `"all"`。
- **"实测"在本文只有一个意思**：本地账本算出来的数，不是官方余量。它出现在三处——明细里的「本实例实测用量」块
  （`showInstanceWindow` 管这块的显隐）、任意供应商的 `window:<provider>` 卡、以及内置的 `token-plan-window`
  （就是千问那条账本）。三者默认窗口 7 天，`windowDays` 可改；重置时刻由本实例第一次调用起算，
  点「清本周期账本」重新起算。

## 配置

主配置 `~/.dsh/token-plan-quota.json`——宿主每次查询前重读，**改完不用重启**（也可写在插件行 `config`，
JSON 优先级更高）：

```json
{
  "autoDetect": true,
  "refreshMinutes": 10,
  "pollSeconds": 10,
  "panelScope": "current",
  "debug": false
}
```

下表 19 行：18 行是代码里 `DEFAULTS` 的键，另 1 行 `moonshotRegion` 不在 `DEFAULTS` 中——它是 Moonshot 源
通过 `regionConfigKey` 读取的区选择器。**一个键一行，单位都写在键名里**（`Minutes`/`Seconds`/`Ms`）。

| 键 | 默认 | 含义 |
|---|---|---|
| `autoDetect` | `true` | 按宿主在用路由自动补齐数据源。**写 `sources` 不会把它关掉**：你写的是一条叠加层，检测照样补齐你没管的路由，所以 `"sources": []` 只表示"我没额外要求"（与不写等价）。设 `false` 才是"以你写的为唯一事实"：此时什么都不写 → 回落到内置两条 `deepseek-balance` + `token-plan-window`；写 `"sources": []` → **一张卡都没有** |
| `sources` | 无（交给检测） | 数据源清单，按顺序显示。内置源名（8 个）：`deepseek-balance`（DeepSeek 余额）、`token-plan-console`（千问控制台余量，Cookie）、`token-plan-window`（**千问本地实测账本**，非自建窗口）、`moonshot-balance`（Moonshot/Kimi 余额）、`openrouter-credits`（OpenRouter 余额）、`account-balance`（阿里云账户余额）、`fr-instances`（阿里云资源包实例列表）、`resource-package`（阿里云资源包额度列表）；或简写 `window:<provider>`（给任意供应商挂实测窗口）；或完全自定义对象（见[自定义源](#自定义源)） |
| `moonshotRegion` | `china-mainland` | Moonshot 区：`china-mainland`（`api.moonshot.cn`，CNY）/ `international`（`api.moonshot.ai`，USD）。**两区 Key 不互通**，选错会 401（卡片会直接提示切区）；host 与币种成对切换，不做自动探测 |
| `refreshMinutes` | `10` | 官方源的快照缓存分钟数：TTL = 分钟 × 60 秒，**下限 15 秒**，所以 `"refreshMinutes": 3` 就是每 3 分钟回源一次（想比 1 分钟更勤没有意义，秒级刷新请看 `pollSeconds`）。点面板「更新于」或带 `?fresh=1` 可强制回源 |
| `pollSeconds` | `10` | 前端轮询秒数——只管界面多久取一次快照，实测与吞吐每次实时重算；调小让徽标速度更跟手，不会多打上游 |
| `panelScope` | `current` | 明细面板范围：`current` **只列 `providers` 里含当前路由 id 的卡** + 本实例实测；`all` 列全部源。所以 `current` 下**看不到阿里云那三条**——账户余额与资源包没有供应商归属，永远不进按路由过滤的视图，要看它们就设 `"all"` |
| `showInstanceWindow` | `true` | 明细里是否带「本实例实测用量 + 限流重试观测」这一块。它**不是**某个数据源的开关，也不影响 `window:<provider>` 卡 |
| `exposeTool` | `true` | 是否注册模型可调用工具 `token_plan_quota` |
| `debug` | `false` | 明细里常驻回显上游响应的**字段骨架**（值打码、跳过凭据字段名）。与 `GET /token-plan-quota/probe?source=<id>` 输出同一份东西，区别是 debug 常驻、probe 按需单次且不用改配置 |
| `endpoint` | `business.aliyuncs.com` | 仅阿里云费用中心三个源使用：OpenAPI 接入点（国际站要换）。会自动剥掉 `https://` 与末尾斜杠 |
| `allowUndeclaredHosts` | `false` | 出站白名单的**用户侧开关**：默认只允许 `dshhub.permissions.network` 里声明过的 8 个主机（回环除外），其余在请求发出前就被拒（错误码 `HostNotDeclared`）。自定义源指向别处时必须显式打开——打开后快照里会留一条 notice，不静默降级。内置源报这条说明代码与声明不一致，`lib/index.js` 的 `DECLARED_HOSTS` 与 `package.json` 要一起改 |
| `regionId` | 无 | 仅阿里云使用：OpenAPI 的 `RegionId` |
| `accessKeyIdRef` | `ALIBABA_CLOUD_ACCESS_KEY_ID` | 仅阿里云使用：AccessKeyId 的凭据引用名 |
| `accessKeySecretRef` | `ALIBABA_CLOUD_ACCESS_KEY_SECRET` | 仅阿里云使用：AccessKeySecret 的凭据引用名 |
| `securityTokenRef` | 无 | 仅阿里云使用：STS 临时凭据的引用名（用 AK/SK 长期凭据时留空） |
| `configPath` | `$DSH_HOME/token-plan-quota.json` | 外部 JSON 配置位置（`~/` 落 OS 家目录，`$DSH_HOME/` 落 harness 家目录） |
| `usagePath` | `$DSH_HOME/token-plan-quota.usage.json` | 本实例账本落盘位置 |
| `minIntervalMs` | `1200` | 出站最小间隔（毫秒），全局节流 |
| `timeoutMs` | `15000` | 单次上游请求超时（毫秒，下限 1000） |

### 只想关掉某一张卡

`sources` 的一项既可以写成内置源名（字符串），也可以写成对象——**选项只有对象形式带得上去**，所以想关掉某一张卡，
要把那一项改成对象。**这是一条排除项，不是整张清单的替换**：`autoDetect` 保持 `true`，其余卡照常补齐。

```json
{
  "autoDetect": true,
  "sources": [
    { "id": "deepseek-balance", "enabled": false },
    { "id": "fr-instances", "enabled": false }
  ]
}
```

- **对自动检测开出来的源同样有效**：写了 `{"id":"moonshot-balance","enabled":false}`，检测就不会再把这张卡补
  回来，诊断块的 `skipped` 里留一条 `disabled-by-config`——「没认出来」和「你关掉了」这两种情况得分得开。
- **关的是这一条源，不是这个供应商**——但"这家还剩什么"取决于它在不在规则表里，这一条必须说白：
  - **认得出的厂家**（DeepSeek / Moonshot / OpenRouter）各自只有一条官方源，关掉它**就没有这张卡**：
    实测兜底窗口只挂在**认不出**的路由上，不会因此补上来。
  - **千问是唯一的例外**：它同时有 `token-plan-console`（官方）与 `token-plan-window`（实测）两条预设，
    关掉前者，后者照旧在。
  - **认不出的厂家**（MiniMax 这类）本来就只有一张 `window:<provider>` 卡，要关它就点它的名：
    `{"id":"window:minimax-cn","enabled":false}`（`<provider>` 就是路由 id，见上节）。
- 不想逐张关，就把 `autoDetect` 设 `false` 之后自己列全清单。两种做法都行，**你写过的永远赢**。

### 凭据

按顺序解析：**DSH 凭据服务 → 环境变量 → `~/.dsh/.credentials.yaml` → `~/.dsh/.env`**，每次查询重新解析，
换 Key 不用重启。

| 源 | 引用名 |
|---|---|
| DeepSeek | `DEEPSEEK_API_KEY` |
| Moonshot | `MOONSHOT_API_KEY`（须与区配对；自动检测会优先沿用路由 profile 点名的引用名，如 `MOONSHOT_CN_API_KEY`） |
| OpenRouter | `OPENROUTER_API_KEY`（`sk-or-v1-…`） |
| 千问 Token Plan 余量 | `BAILIAN_CONSOLE_COOKIE`（+ 可选 `BAILIAN_CONSOLE_SECTOKEN` 兜底） |
| 阿里云 | `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` |

Key 引用名可在 `sources` 条目里用 `bearerRef` / `cookieRef` 覆盖。

想看到千问的**真实余量**（订阅页那种「剩余量 76.2% / 总额度 10,000」）：登录打开
[订阅页](https://platform-home.qianwenai.com/analytics/token-plan/individual) → 按 F5 → F12 Network（勾**保留日志**）
→ 筛选框输入 `cs-data` → 复制 **`cs-data.qianwenai.com/data/api.json`**（`action=BroadScopeAspnGateway`）那条请求的
**整行 `Cookie:`** → 存成一行 `BAILIAN_CONSOLE_COOKIE: <内容>` → 点面板「更新于」刷新。
两条硬要求，缺一条网关都会回 `NotLogined`：

- **必须是插件实际调用的那台主机**（`cs-data.qianwenai.com`）。Cookie 按域隔离，从别的控制台域名抄来的用不了
  （2026-10 实机踩过两次：先按旧提示筛 `bailian-cs` 一条请求都筛不出来；改成从阿里云那侧抄来
  `cna/cnaui/… + login_aliyunid_csrf`，网关照样拒绝）。
- **里面必须有登录票据 `login_qianwenai_ticket`**。只有反爬 Cookie 集（`cna`、`cnaui`、`aui`、`sca`、`atpsida`、
  `tfstk`、`isg`、`partitioned_cookie_flag` 这类）而没它，就说明抄的是另一条不带登录态的请求。

Cookie 通常能用几周，过期后该源报错并自动退回实测卡，重贴即恢复。被拒时面板里的错误会**列出收到的 Cookie 名
与插件打的主机**，照着对一下就知道抄错在哪。

### 自定义源

上游给了新端点、或你想把某个控制台接口接进来，不用等本插件更新——`kind: "single"` + `fields`/`derive`
就能覆盖"余额是差值"这类形态：

```json
{
  "sources": [{
    "id": "my-balance", "label": "我的余额", "kind": "single", "metric": "money",
    "url": "https://example.com/api/account", "method": "GET", "bearerRef": "MY_API_KEY",
    "unit": "USD",
    "fields": { "credits": ["data.total_credits", "total_credits"], "usage": ["data.total_usage", "total_usage"] },
    "derive": { "total": "credits", "used": "usage", "remaining": "credits - usage" },
    "providers": ["my-provider"]
  }]
}
```

`derive` 只认「一个 `+`/`-`、两侧是 `fields` 里的引用名或数字」；**任何一个操作数取不到就整条空**，绝不猜数补上。
`fields` 的槽位名随你起（`credits`/`usage` 只是示例），它同时也是 `derive` 表达式里能引用的变量名；
每个槽位给多条候选路径是有意为之（同一家 API 版本间 `data` 信封加不加都见过）。

#### 手写条目的全部可用键

**认不出的键不会报错，只是没作用**（条目对象是原样合并进去的），所以键名打错字的表现是"卡片空着"而不是启动失败。
这张表是权威清单：

| 键 | 用在哪 | 说明 |
|---|---|---|
| `id` | 必填 | 源标识，同时是缓存键和 `probe?source=` 的参数。两个条目用同一个 `id` 会共用缓存并叠成两张卡（日志会 warn），第二个请改名 |
| `kind` | 建议写 | `single`（一次读数，不写即此值）/ `list`（多条资源列表）/ `window`（本实例实测窗口，不打网络） |
| `label` / `labelEn` | 标题 | `label` 是卡片标题，不写时回落成该条目的 `id`。界面语言是英文时**优先读 `labelEn`**（面板卡头、多窗口计量条都算），拿不到才退回 `label`——客户端不替你翻译，想要英文标题就在条目里写 `labelEn` |
| `url` | `single` / `list` | 端点。写了 `url` 即 HTTP 源；不写 `url`、也不是内置源名，则按阿里云 OpenAPI **RPC** 处理（要 `action` + `version`） |
| `method` | HTTP | 默认 `GET` |
| `headers` | HTTP | 追加请求头，与 `accept: application/json` 合并 |
| `jsonBody` / `formBody` | HTTP（POST 用） | JSON 请求体 / `application/x-www-form-urlencoded` 请求体 |
| `bearerRef` / `cookieRef` | HTTP | 凭据引用名。Bearer 型只认 `bearerRef`，Cookie 会话型只认 `cookieRef`——**不会拿套餐 Key 去顶替 Cookie**（反之亦然），因为那只会得到一张误导性的错误卡 |
| `action` / `version` / `params` | RPC | OpenAPI 的 Action、版本号、查询参数（`params` 会被扁平化）。RPC 型缺 `action` 或 `version` 会直接出错误卡 |
| `paginate` | RPC | `{ "mode": "page", "request": "PageNum", "pageSize": "PageSize", "total": ["TotalCount"] }`，或 `{ "mode": "token", "request": "NextToken", "response": ["NextToken"] }`；`page` 模式最多翻 10 页 |
| `fields` | `single` | 变量名 → 候选路径数组，取到数的才进 `derive` 作用域 |
| `extract` | `single` | 上游直接给值时的路径：`remaining`、`total`、`unit`。它们也能被 `derive` 引用 |
| `derive` | `single` | 只有 `remaining` / `total` / `used` 三个键会被消费；`used` 不写时，`remaining` 与 `total` 都在就自动算差值 |
| `extra` | `single` | 副信息，键名随你起；`toppedUp`/`granted`/`cash`/`credit`/`quotaLimit` 有既定文案 |
| `list` / `item` | `list` | `list` 是数组候选路径；`item` 内可给 `name`/`id`/`remaining`/`total`/`used`/`unit`/`status`/`expiresAt`/`startsAt`/`cycleType`/`capacityType`/`haystack` |
| `metric` | 显示口径 | `money` / `credits` / `count`。默认值按构建器不同：手写 `single` 源是 `money`，`list` 源是 `credits`，`window` 是 `count`——**要按 token/次数显示就显式写** |
| `unit` | 显示 | 上游不给单位时的兜底（如 `USD`）；`metric:"money"` 且上游不给时兜到 `CNY` |
| `providers` | 面板归属 | 这条属于哪些**路由 id**（见上文「provider 就是路由 id」）。`panelScope:"current"` 下，列表里没有当前路由就不显示这张卡；写了却一条都对不上在用路由，宿主日志会提醒（免得对着空白面板瞎猜）；`window:<provider>` 简写会自动填 |
| `windowDays` | `window` | 实测窗口天数，默认 7，最小 1 |
| `regions` / `region` | 多区供应商 | `regions` 是「区名 → 该区的字段覆盖（host 与币种成对换）」，`region` 选哪一区；写错的区会 warn 并退回第一个 |
| `enabled` | 任意 | `false` 停用这一条（临时关源不必删整段） |

**表外的键是内置预设专用，手写别照抄**：`builder`、`apiPrefix`、`consoleSite`、`gatewayAction`、`gatewayProduct`、
`infoUrl`、`secTokenRef`、`dashboardURL`、`regionConfigKey`、`errorHints`、`keywords` 走的是各家特定的签名/CSRF/
信封流程，只对相应预设成立。特别地：面板里的**多行窗口**（当前账期那一行 +「5 小时」；账期是
「本月」还是「7 天」取决于上游这个账号真回了哪个读数）目前**只有
`token-plan-console` 会产生**，手写条目一条只有一个读数；想显示两个窗口，就写两条源。
实测窗口用 `{"kind":"window","providers":["my-provider"],"label":"我的窗口","windowDays":30}`，
或简写 `window:<provider>`。接入新厂家的完整核对流程见 [`docs/adding-a-provider.md`](docs/adding-a-provider.md)。

#### 卡片空着、又没报错，按这个顺序查

1. `GET /token-plan-quota/probe?source=<id>`（或临时开 `"debug": true`）——先看上游**实际**回了哪些字段名；
   零配置安装里源是自动检测出来的、不写在配置里，`probe` 查不到时会**补跑一次检测**再查（2026-09-23 之前
   它对这类源永远回 `unknown-source`，等于把最有用的诊断入口关掉了）。这次「余量不见了」就是这么定位的：
   `usage` 实际回的是 `per1MonthPercentage`，而代码只认 `per1Week` / `per5Hour`。
2. 对照上面那张权威键表逐字核对键名：**认不出的键不报错、只是没作用**，表现就是卡片空着而不是启动失败；
3. `fields` 的路径是否真命中了这份响应（信封加不加 `data` 见过两种），`derive` 引用的名字是否都取到了数
   ——任一操作数缺失就整条空；
4. 如果这张卡**根本没出现**，那通常不是空卡问题：凭据解析不到的源整个不开（不挂错误卡占地方），
   或者被 `panelScope: current` 挡在视野外。看快照的 `detection` 块，它会写明谁被跳过、为什么。

## 吞吐速度（实测，不估算）

宿主包一层 `llm/stream`，记录**首分片到 usage 分片**的活跃时长。两个口径严格分开：

- **速度（生成/解码）**：`lastTps` = 输出 tokens ÷ 活跃秒；`genTps` = 近 5 分钟 Σ输出 ÷ Σ活跃秒
  （活跃总时长 <1 秒不出数，防超短流外推）。**缓存读不计入分子**——否则一次 36 万缓存命中的调用会把
  速度吹成几万 tok/s；
- **吞吐（量）**：`tpm60` / `tokens300` = 全部 tokens（含缓存读），计费与搬运视角；`outTps60` 是近 60 秒输出均速。

滑动窗口随账本落盘，宿主重启后最近 5 分钟的实测还能带回。速度**不占徽标位**（小胶囊只露渐变条与余量数字），
取数优先级：最近单流（90 秒内）→ 近 60 秒输出均速 → 近 5 分钟生成速度；无新鲜活动不显示。
面板的「吞吐」一行只报**这一实例的聚合值**，不逐家再列一遍速度——逐家数据仍在快照里，需要时走 `action=status`。

## 只读路由与模型工具

| 端点 | 作用 |
|---|---|
| `GET /token-plan-quota/summary` | 快照（官方源吃 TTL；`?fresh=1` 强制回源）；含 `detection` 诊断块 |
| `POST /token-plan-quota/refresh` | 官方源回源 |
| `POST /token-plan-quota/reset` | 清零本实例实测统计（含窗口锚点） |
| `GET /token-plan-quota/probe?source=<id>` | 上游响应字段骨架（排查用） |

只接受同源请求，**永不回传密钥**。模型侧有 `token_plan_quota` 工具（`status` / `refresh` / `reset`）。

配合 429：`settings.yaml` 里给该 provider 配 `retryPolicy`（把 `QUOTA` 纳入 `retryableCodes`，
`initialDelayMs == maxDelayMs` + `jitterRatio: 0` 可把退避压成固定间隔）。明细的「自动重试观测」显示真实
发生的重试次数、等待秒数与生效策略——读 `llm/retry` 持久事件的 `policyKey`，不估算。

## 已知边界

- 实测窗口只含**经过本 DSH 实例**的调用：别的设备/工具消耗的额度不在内，官方折算率也不公开，
  所以它只报 token/次数，不报「剩余额度」。
- 上游控制台做过「额度重置」后，本实例窗口不会自动感知，用 `token_plan_quota`（`reset`）重新起算。
- 观测计数是本次进程启动以来的；持久事实仍在会话日志里。
- 座位退化：外壳没声明 `conversation.input.left` 时退到 `conversation.input.dock`；
  老外壳拿不到 react-dom 时面板退回内联 CSS 锚定，拖拽悬浮禁用。
- Moonshot / OpenRouter 的字段名**尚未用真实 Key 核对**（只有官方文档 + 端点存在性背书）。

## 明确不做的三类

以下形态需要读取其它 CLI 的本地登录态或额外强凭据，因权限与账号风险**不在本插件射程内**，
也不接受相关 PR——需要的人请用上面的「自定义源」自己配：

- 智谱 GLM 团队模式（`Bigmodel-Organization` / `Bigmodel-Project`）
- Kimi Code 的浏览器 Cookie / `~/.kimi-code/credentials/*`
- Codex·ChatGPT / Gemini CLI 的 OAuth token（`~/.codex/auth.json`、`~/.gemini/oauth_creds.json`）

## 免责声明

千问 Token Plan 的余量卡走的是**控制台数据网关（Cookie 会话），不是公开发布的官方 API**：
上游随时可能改动或拒绝；若其服务条款禁止此类访问，请**不要启用**该源（把 `token-plan-console` 从
`sources` 去掉，或关掉 `autoDetect` 后不写它）。Cookie 只从本地解析、只在进程内使用、
**绝不进任何路由响应**——细节见 [`SECURITY.md`](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/SECURITY.md)。

## 开发

```bash
npm run check                      # 下面七步一次跑完
node test/host.mjs                 # 437 项，离线
node test/client.mjs               # 240 项，假 React/DOM/fetch
node test/guards.mjs               # 148 项，门禁自身的行为矩阵（应报 / 应放行）
node scripts/check-manifest.mjs    # 清单自检（安装性、出站主机、许可证、零依赖）
node scripts/check-docs.mjs        # README 的可核实声明必须与代码一致
node scripts/check-refs.mjs        # 活文档里不得出现「文件:行号」引用
node scripts/check-submission.mjs  # 插件目录站投稿条目的自检
```

`check-docs` 不是装饰：它把「DEFAULTS 18 个键、配置表 19 行、8 个数据源、声明 8 个出站主机、测试 437/240 项 + guards 148 项」这些写在 README
里的数字拿去和代码与实跑结果对，**数字漂了就 CI 红**（已用反向用例验证它真的会失败）。

测试跨平台（临时目录取 `os.tmpdir()`，不依赖真实 `~/.dsh`），CI 跑 node 20/22 × ubuntu/windows/macos，
外加"解包后能加载"的冒烟（`npm pack` → 解 tar → `import lib/index.js`）。
贡献流程与产品口径见 [`CONTRIBUTING.md`](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/CONTRIBUTING.md)，发版三步（GitHub topic / npm / 插件目录站）见
[`RELEASE.md`](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/RELEASE.md)。

这个仓库自己也按一套写下来的工程循环在走。这些目录在仓库里，但**不随 npm 包发布**：`specs/`（三条规格周期——
ROADMAP 对账、发布面链接、门禁加固）、`workflow/`（相位模板与三 lens 评审清单）、`AGENTS.md`（给 agent 的常驻指令）、
`.specify/`（Spec Kit 的规格与宪法）。要看原文走绝对链接：
[workflow](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/workflow/README.md)、
[AGENTS.md](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/AGENTS.md)、
[宪法](https://github.com/xinghe-1018/dsh-token-plan-quota/blob/main/.specify/memory/constitution.md)。

## 许可

[MIT](LICENSE)
