# dsh-peak-balance

[![ci](https://github.com/VviLliAm-qwq/dsh-peak-balance/actions/workflows/ci.yml/badge.svg)](https://github.com/VviLliAm-qwq/dsh-peak-balance/actions/workflows/ci.yml)

**中文** · [English](README.md)

DeepSeek 峰谷计费时钟 · 实时余额 · 每轮花费 · `/hist` 历史用量方格图，显示在 [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) 里。

```
⚡ 峰时 09:00-12:00 · 距谷时 1h23m · 本轮 ¥0.0234 · 余额 ¥42.10
```

（顺序是 时段 → 倒计时 → 本轮 → 余额：终端一窄，最先被截断的是余额，而每轮花费是运行中一直在变的那一项。模型正在作答时，「本轮」显示的是**实时**花费，写成 `本轮·计费中 ¥…`，随每次用量上报刷新；一轮结束后定格为 `本轮 ¥…`。）

开启峰时警告模式后，处于高峰时段时这一行会变成一个按所选颜色脉动闪烁的边框：

```
╭──────────────────────────────────────────────────────────╮
│ ⚡ 峰时 · 距谷时 1h23m · 本轮 ¥0.0234 · 余额 ¥42.10   ▂▃▄▅▆▇ │
╰──────────────────────────────────────────────────────────╯
```

输入 `/hist`（或 `/tokenhistory`；`/th` 也行但要带个空格，见下文）打开历史看板——一个占满终端的场景，形状像 GitHub 的贡献图：

```
╭─ 🐋 Token 历史  总 token  26w  子代理 计入 ────────────────────────────── ✕ ─╮
│ 更新于 12:04:11 · 341 会话 · 6,706 事件 · 9 活跃天数                          │
│ ───────────────────────────────────────────────────────────────────────────── │
│      6月      7月      8月      9月                                           │
│ Mon  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│ Wed  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│ Fri  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│                       ▲                                                       │
│ 总 token 少 ▢▢▢▢▢ 多 · 峰值 251,442,584                                       │
  ╭─ 2026-09-11 周五 ───────────────────────────────────────────────────────╮
  │ token       未命中输入 2,065,340 · 缓存读 224.3M · 输出 1.5M              │
  │ 花费        ¥18.8494                                                      │
  │ 缓存命中    99.1% · 子代理占比 12.4%                                      │
  │ 模型        deepseek-flash 227.9M · deepseek-v4-pro 3.2M                  │
  ╰───────────────────────────────────────────────────────────────────────────╯
│ 总计        1,079,834,040 · 总花费(估) ¥64.6867 · 缓存命中 99.0%              │
│ 子代理      36/341 会话 · 945 事件 · 峰值日 2026-09-11                        │
│ ───────────────────────────────────────────────────────────────────────────── │
│ 模型                             总 token    花费(估)   命中率 费率来源       │
│ ───────────────────────────────────────────────────────────────────────────── │
│ deepseek-flash                     587.3M      ¥33.94    99.3% 内置价目       │
│ deepseek-v4-flash-vision-exp       412.6M      ¥24.53    98.8% 内置价目       │
│ deepseek-v4.1-flash-expires…        68.2M           —    98.5%     未知       │
│ deepseek-v4-pro                      8.0M       ¥4.28    95.4% 内置价目       │
│ deepseek-v4-flash                  457.2k     ¥0.0975    90.1% 内置价目       │
│ ←/→ 前后周 · ↑/↓ 前后天 · t 今天 · m 指标 · w 跨度 · s 子代理 · r 刷新 · q/Esc │
╰───────────────────────────────────────────────────────────────────────────────╯
```

## 功能

| 功能 | 说明 |
| --- | --- |
| 峰谷时钟 | 显示当前计价时段与切换到下一时段的倒计时（北京时间周一至周五 `09:00-12:00`、`14:00-18:00` 为高峰，周末全天谷时）。 |
| 账户读数（通用） | 按**当前对话实际使用的 provider** 显示余额或套餐额度：官方余额、订阅套餐的 5 小时/周/月窗口、中转站的额度与限额。provider 由会话日志的 `request/header.config.provider` 判定，base URL 与密钥引用经宿主接缝解析；开机恢复的对话在发出第一个请求前不会报告自己的路由（宿主重放历史时不再发事件），此时 `auto` 跟随宿主持久化的 `/model` 路由——也就是下一个请求将要走的那条。没有可查接口的 provider 直接不显示，**绝不猜数字**。 |
| 每轮花费 | 刚结束那一轮对话的花费：provider 有消费计数器时显示**实测扣减**（如 Command Code 的 credits、OpenRouter 的 key 用量、中转站的已用额度），否则按价目估算，都没有就显示「费率未知」。数字跟着你**当前聚焦的对话**走。 |
| 额度诊断 `/quota` | 一条命令看清算的是什么、谁在应答、上次为什么失败，以及本进程能路由到的全部 provider（`/quota check <provider>` 现场探测）。 |
| 峰时警告 | 可选。高峰时段生效时，状态行变成圆角边框，边框、时段标签与右侧波形按设定颜色脉动。 |
| 历史方格图 `/hist` | 全屏场景：一天一格、按当天用量深浅着色，鼠标悬停出当日明细；键盘按**方格**挪动（`←/→` 前后一周、`↑/↓` 前后一天、`t` 回到今天），另有 `m` 指标、`w` 跨度、`s` 子代理、`r` 刷新、`q`/`Esc` 返回。 |
| 总计与模型维度 | 总计：总 token、总花费（估）、缓存命中率、活跃天数、会话数、子代理占比、峰值日；模型表：每个模型的总用量、总花费、总缓存命中率与费率来源。总计行统计**全部历史**（不随方格显示的跨度变化），行内标注「全部历史」以说明这一点。 |
| 自定义费率 `/hist price` | 内置价目表没收录的模型由你自己补单价；未设置前只显示 token 并标注「费率未知」，绝不猜金额。 |
| 跟随界面语言 | 与 dsh-tui 的 `/lang` **即时联动**：状态行、历史场景、命令回执都跟着切（宿主把选择写进 `dsh-tui` 设置命名空间，插件监听 `settings/updated` 事件；没挂该命名空间的宿主由 1 秒轮询 `~/.dsh-tui/lang.json` 兜底）。设置卡片与命令补全描述本来就中英双语。 |
| 设置子页 | `/settings` 的 **Peak & Balance（峰谷与余额）** 卡片新增 **Token history（历史用量）** 子页，共 10 个选项（含场景版式与色阶）。 |

## 安装

```sh
# 从 npm 安装
dsh plugin --profile dsh-tui add dsh-peak-balance

# 或直接从本仓库的本地目录安装（pnpm 会把目录打包复制进 profile）
dsh plugin --profile dsh-tui add file:/到本仓库的绝对路径/dsh-peak-balance
```

该命令会把 bundle 行追加进 profile 的 `dsh.profile.bundles`。之后在 dsh-tui 内执行 `/restart` 让 profile 载入新行；重启后 `/settings` 即可看到设置卡片。

> 不建议用符号链接（`link:` 或目录联接）方式安装：Node 会按插件的真实路径解析依赖，`@deepseek-ai/*` 必须能从该路径向上找到。`file:` 与 npm 安装都会在 profile 内留下真实目录，这正是宿主期望的布局。
>
> 更新本地 `file:` 安装：pnpm 会缓存本地目录依赖，只跑 `add`/`update` **不会**拾取改过的源码。请先升版本号，再卸载重装以刷新 profile 内的副本：
>
> ```sh
> dsh plugin --profile dsh-tui remove dsh-peak-balance
> dsh plugin --profile dsh-tui add file:/到本仓库的绝对路径/dsh-peak-balance
> ```

## 设置项（主卡片四项 + 历史用量十项 + provider 额度七项）

主卡片：

| 字段 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| Show balance / 显示余额 | boolean | `true` | 状态行是否显示账户那一段（余额或套餐额度）。 |
| Show per-turn cost / 显示每轮花费 | boolean | `true` | 是否显示当前这轮的花费：作答进行中显示实时估算，轮次结束后定格（provider 有计数器时显示实测扣减）。 |
| Peak-hour warning / 峰时警告模式 | boolean | `false` | 高峰时段是否把状态行变成闪烁边框。 |
| Warning color / 警告色系 | select | `red` | 边框颜色：红 `red`、橙 `orange`、黄 `yellow`、绿 `green`、青 `cyan`、蓝 `blue`、紫 `purple`。 |

**Provider quota（第三方 provider 额度）** 子页：

| 字段 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| Quota provider / 额度来源 | text | `auto` | `auto` 跟随聚焦对话的 provider；恢复的对话还没发出第一个请求时，先跟随宿主持久化的 `/model` 路由；填 provider id（如 `commandcode`、`openrouter`）固定其中一个；`off` 隐藏账户那一段。 |
| Quota metric / 额度口径 | select | `auto` | `auto`（最紧的窗口）、`balance`、`window5h`、`windowWeekly`、`windowDaily`、`windowMonthly`、`planRemaining`、`keyLimit`、`periodSpend`、`lifetimeSpend`（累计已用）、`rotate`（轮换，每 8 秒换一个口径）。 |
| Turn spend / 每轮花费口径 | select | `auto` | `auto`（有计数器就实测）、`measured`、`estimate`。 |
| Unofficial endpoints / 允许非公开端点 | boolean | `false` | 是否允许读取厂商未公开文档的额度端点（多为逆向控制台接口）。 |
| Provider spec file / provider 声明文件 | text | 空 | 描述本包未内置适配器的 provider 的 JSON 文件；留空用 `~/.dsh-tui/dsh-peak-balance-providers.json`。 |
| Billing mode / 计费方式 | select | `auto` | 账户读数的形态：`auto` 按 provider 的声明（订阅=百分比、按量=金额），也可强制 `money`（金额）或 `plan`（百分比）。 |
| Percentage base / 百分比基数 | select | `meter` | 套餐百分比的分母：`meter` 用状态行当前显示的那个窗口，`monthly` 用整个月度额度。 |

**Token history（历史用量）** 子页：

| 字段 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| Grid metric / 方格图口径 | select | `tokens` | 方格深浅表示什么：`tokens` 总 token、`cost` 花费、`output` 输出 token、`cacheMiss` 缓存未命中输入。 |
| Time span / 时间跨度 | select | `26` | 方格图显示多少周：`13` / `26` / `53`。 |
| Count subagents / 计入子代理 | boolean | `true` | 子代理会话也真实消耗 token；关掉后只统计你自己跑的对话，总计行会写明排除了多少。 |
| Week starts on / 每周起始日 | select | `mon` | 方格图第一行是 `mon` 周一还是 `sun` 周日。 |
| Grid palette / 方格色阶 | select | `github` | `github` GitHub 绿、`blue` 蓝色系、`theme` 跟随当前主题强调色。 |
| Scene layout / 场景版式 | select | `card` | `card` 圆角边框卡片 + 分区辅助线；`plain` 无边框（省 2 行 4 列）。终端太小时 `card` 自动退化为 `plain`。 |
| Hover: tokens / 悬停：token 分项 | boolean | `true` | 当日明细是否显示 未命中输入 / 缓存读 / 输出 分项。 |
| Hover: cost / 悬停：花费 | boolean | `true` | 是否显示当天估算花费。 |
| Hover: cache hit rate / 悬停：缓存命中率 | boolean | `true` | 是否显示当天缓存命中率。 |
| Hover: models / 悬停：模型明细 | boolean | `true` | 是否显示当天用了哪些模型。 |

设置写入 `dsh-peak-balance` 命名空间，改完立即生效，无需重启。**场景内的 `m` / `w` / `s` 就是改设置**（会写回 `dsh/settings.yaml` 并立即生效，重开 TUI 仍然有效）：按下去先本地翻转，写入失败时只保留本次会话内的改动并记一条日志。

## `/hist` 历史用量与花费

```sh
/hist                                 # 打开方格图（推荐，不会与内置命令冲突）
/tokenhistory                         # 同上（长别名）
alt+h                                 # 同上（不用打字；聊天状态下生效）
/th                                   # 也行，但要打「/th 」带一个空格，见下
/hist price                           # 列出已自定义的费率 + 费率未知的模型
/hist price set <model> <hit> <miss> <out> [peakHit peakMiss peakOut]
/hist price rm <model>
/hist price clear
/quota                                # 额度诊断：来源、适配器、口径与上次失败原因
/quota check <provider>               # 现场探测一个 provider（provider 省略则用当前目标）
/quota meter windowWeekly             # 切换状态行口径（等于改设置）
```

### 为什么 `/th` 单独回车会去切主题

这是**宿主行为，插件改不了**，如实说明：dsh-tui 的输入框在斜杠补全菜单打开时，回车执行的是**当前高亮的那条建议**，不是你打的那行字（`PromptInput.js` 的 `handleEnter`）；而补全列表是「内置命令在前、插件命令追加在后」，且打字时选中项固定回到第 0 条。打 `/th` 时匹配到 `theme`、`thinking` 和我们的 `th`，高亮落在 `theme` → 回车就切了配色。

可用的四条路：

| 入口 | 说明 |
| --- | --- |
| `/hist` | **推荐**。`hist` 不是任何内置命令的前缀，打 `/hist` 时菜单里只有它自己，回车直接执行。 |
| `/tokenhistory` | 同样不冲突。 |
| `alt+h` | 聊天状态下直接打开场景，不用打字。 |
| `/th ` + 回车 | `/th` 后面跟一个**空格**：菜单会因为「已经是子命令位置」而关闭，回车就走 `th` 命令。 |

> 注意：如果将来装了名字以 `hist` 开头的**技能**（skill），它也会以同样的方式抢 `/hist` 的回车——那是同一套宿主补全逻辑，届时换个名字即可（代码里是一个常量）。

### 键盘

| 按键 | 作用 |
| --- | --- |
| `←` / `→` | 挪到**左边/右边那一格**（同一星期几，前后一周） |
| `↑` / `↓` | 挪到**上边/下边那一格**（同一列，前后一天） |
| `t` | 回到今天 |
| `m` / `w` / `s` | 切换 指标 / 跨度 / 子代理（会闪烁对应徽章并写回设置） |
| `r` | 重新扫描 |
| `q` / `Esc` | 返回对话 |

到方格图边界或「未来」的格子会**停住**（不绕回）。上下左右都是纯几何挪动，不再按「天/周」两个维度跳。选中格会被提亮，并在方格图下方用 `▲` 指出所在列。

**子代理开关的反馈**：场景标题栏有常驻徽章（`子代理 计入` / `子代理 不计入`，计入时是绿色），按下 `s` 会反色闪烁 1.2 秒；关掉时总计行还会写明「已排除 N 会话 / M 次上报」，而不是只让数字变小。

**数据来源是本机会话日志**（`$DSH_HOME/sessions/`）。日志里每一个 `assistant/message` 事件都带着 DeepSeek 为该次请求返回的用量（`inputTokens` / `cacheReadTokens` / `outputTokens` / `cacheWriteTokens`），插件按**事件自身的时间戳**落到北京时间的自然日与高峰/空闲档，再按模型归档。因此：

- **token 是官方上报的真实数字**，不是本地估算；
- **金额是估算**（官方 API 只返回 token，不返回钱），用内置价目或你自定义的单价换算，并明确标注；
- 只覆盖**本机 dsh 的用量**，其他客户端或网页版的调用不在其中；
- 覆盖范围随本机日志保留策略而定（本机现有日志从 2026-09-04 起）。

**子代理**默认计入（它们花的是真钱），表里可分辨，也可以关掉。

**未收录模型**（内置价目表没有的）只显示 token，金额显示「—」，等你在 `/hist price set` 里给定单价后才参与计价。自定义费率立即写入 `~/.dsh-tui/dsh-peak-balance-rates.json`，并同时影响历史看板与状态行的每轮花费。`<hit> <miss> <out>` 是**空闲时段**的三个单价（元/百万 tokens），高峰时段默认按官方规则取两倍；要给高峰单独定价时再补三个数字。模型名可以写成 `provider:model`（如 `commandcode:deepseek/deepseek-v4.1-flash`），这样同一个模型在不同账户下可以有不同单价；不带前缀的写法始终有效。

**准确性的两个实现细节**（都能在真实日志上复现）：

1. 会话日志是**多帧 zstd**（每次追加一个独立帧）。`zlib.zstdDecompressSync` 只解第一帧，而"扫魔数切帧"会在压缩块内部偶然命中魔数——此时截断解码仍会「成功」并返回残缺内容，静默丢事件。本插件改为解析 zstd 帧头与块头**精确计算帧长**；在本机 341 份日志上，旧写法丢了 282 个事件，新写法全部取回。
2. fork / rewind 出来的会话日志会**物理携带父会话的事件前缀**。插件按日志头的 `seedLength` 截断（首个 `session/end-seed` 事件即落在此处），否则会把父会话的用量重复计一遍——本机 9 份 seeded 日志里是 292 条用量事件（约 4.5%）。

**缓存**：插件激活时就读取增量缓存并直接画出方格图，所以打开场景是瞬时的；随后的扫描只刷新变化的部分。缓存分三层增量：按 `(路径, 大小, mtime)` 跳过没变的日志；日志**只被追加**时，从缓存记下的「已折到第几帧」（`done`，附前缀尾部 16 字节签名校验）继续，**只解压新增的帧**；记录结构升级时由迁移函数就地改写旧缓存，而不再整份重扫。本机实测（437 份日志 / 112 MB / 19 万帧）：最大的那份日志追加后，重扫从 314 ms 降到 17 ms；插件升级触发的首次扫描从整份重建降到 81 ms。内容已是最新时整轮约 20~30 ms。缓存写在 `~/.dsh-tui/dsh-peak-balance-history.json`（可安全删除，删了会自动重建）；真正慢的只有本机第一次全量重建（约 5~6 秒，其中约 4.3 秒是 19 万个小 zstd 帧的逐帧解码，这是 Node 端的硬开销），它会边扫边落盘，**并且边扫边出图**——最新的日志先扫，打开场景不到一秒就能看到最近数周的真实数据，中途被打断也能续扫而不是重来。场景打开期间每 60 秒在数字背后静默刷新一次（状态行显示「刷新中」），关掉即停。

## 数字是怎么来的

**时段**：空闲时段价格为高峰时段的一半；高峰为北京时间（UTC+8）周一至周五 `09:00-12:00`、`14:00-18:00`，其余时间（含周六日全天）为空闲时段。

**花费**：DeepSeek API 只返回 token 用量、不返回金额，因此每轮花费是**估算**：

```
费用 = 未命中输入 token × 未命中单价 + 缓存命中 token × 命中单价 + 输出 token × 输出单价
```

三项输入侧数字是**并列不重叠**的：`inputTokens` 是未命中缓存的提示词，`cacheReadTokens` 是命中缓存的部分，服务商返回的 `totalTokens` 正是二者相加再加输出。若把命中量当成输入的**子集**去相减（旧写法），会把带缓存的轮次价格算低数倍 —— 2026-09-10 用真实余额扣减核对：实际扣 ¥0.20 的一轮，旧写法估 ¥0.03，上式估 ¥0.23。

每条用量按其请求发生的时刻落入高峰或空闲桶，跨时段的一轮不会被整体按当前时段计价。内置价目（元/百万 tokens）核对日期 **2026-09-10**，来源为官方[模型 & 价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)页：`deepseek-flash` 空闲 0.02 / 1 / 4，高峰 0.04 / 2 / 8；`deepseek-v4-pro` 空闲 0.15 / 4.5 / 13.5，高峰 0.30 / 9 / 27。旧模型名 `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 按 Flash 价计费；`deepseek-v4-pro` 自北京时间 2026-09-14 12:00 起路由到 V4.1-Flash，按 Flash 价计费。价目表未收录的模型显示「费率未知」，只显示 token 而不给错误金额。

**余额**：`GET https://api.deepseek.com/user/balance`（与 dsh-tui 内置 `/balance` 同一只读接口）。密钥经 `credentials` 接缝读取 `DEEPSEEK_API_KEY`（可回退环境变量），仅放入请求头，不写日志、不落盘。

## 第三方 provider 额度

0.4.0 起，账户那一段不再写死 DeepSeek。插件按下面的顺序决定「问谁、怎么问」，任何一步拿不到就静默降级：

1. **provider** —— 当前聚焦对话最近一次 `request/header.config.provider`（`auto` 模式；也可用设置固定或关掉）。开机恢复的对话还没上报过任何 header，此时 `auto` 改用宿主持久化的 `/model` 路由（`~/.dsh-tui/model.json`，即选择器在重启后重新应用的那个文件）；内置的 `deepseek-official` 默认只作最后兜底（宿主完全没保存过选择时）。
2. **端点与密钥** —— provider 自己的设置段（`ctx.llm.listConfigurableProviders()` 给出的命名空间指针）→ 声明文件 → 内置目录快照。密钥经 `credentials` 接缝解析，失败回落同名环境变量。
3. **适配器** —— 按下表选择；都不匹配就显示「无接口」（只有你显式指定了该 provider 时才显示这一行）。

### 内置适配器

| 适配器 | 覆盖的 provider | 口径 | 是否经真实账户核对 |
| --- | --- | --- | --- |
| `deepseek-balance` | `deepseek-official` / `deepseek` | 余额（按接口回报的币种） | ✅ 是 |
| `commandcode-plan` | `commandcode` | 5 小时 / 周窗口、月度套餐余量、本期消费 | ✅ 是（GOAT 套餐实测） |
| `openrouter` | `openrouter` | 余额（credits − usage）、key 限额、日/周/月用量 | ⚠️ 未核对（按官方文档实现，夹具测试） |
| `moonshot-balance` | `moonshotai` / `moonshotai-cn` | 余额（`.cn` 为 CNY，国际站为 USD） | ⚠️ 未核对 |
| `siliconflow-balance` | `siliconflow` 路由 | 余额（接口未标注币种，按原值显示） | ⚠️ 未核对 |
| `openai-billing` | 组合里**声明**的网关/自建路由（One API、New API 等） | `soft/hard_limit_usd`（两字段同值，是总额度）减去 `total_usage`（美分）；单位跟随网关自己的显示设置 | ⚠️ 未核对 |
| `declared` | 声明文件里的任意 provider | 由声明决定 | — |

OpenRouter 的 `/credits` 需要**管理密钥**（普通 key 会 403）；插件会同时请求 `/key`，因此只有普通 key 时仍然显示 key 限额与用量。

### 套餐按百分比显示

账户那一段的**形态跟着 provider 的计费方式走**：按量计费（有货币余额、按 token 扣钱）显示金额；订阅套餐（带上限的滚动窗口 + 月度池）显示**百分比** —— 状态行给出「还剩多少」，以及「本轮用掉了这个窗口的百分之几」。判定顺序是：设置项 `Billing mode` 的强制值 → provider 自己的声明（适配器或声明文件里的 `billing`）→ 按数据推断（有上限窗口且无货币余额即视为套餐）。推断刻意保守：形状不认识的按金额显示，而不是凭空造一个分母。

百分比的**分母**由 `Percentage base` 决定：默认是状态行当前显示的那个窗口（最紧的限额，重置倒计时也在那一行），也可以改成整个月度额度；两边互相兜底，所以只报其中一种的 provider 也有可用的分母。**没有上限就没有百分比**，此时如实退回绝对数值。

```
🌊 谷时·半价 · 周末 · 距峰时 18h26m · 本轮 1.79%(0.2500) · 套餐 GOAT · 5h 剩 92.9%(1.00/14.00) · 距重置 1h00m
```

**窄终端只显示百分比**：每个片段都带一个紧凑写法（`本轮 1.79%`），整行放不下时从右侧开始逐个换成紧凑写法（宿主正是先截尾）。宽度优先取宿主的终端尺寸钩子，没有就用 `process.stdout.columns`；两者都没有时按完整写法渲染，与旧版本一致。

百分比精度随量级变化：小于 10% 给两位小数（订阅的一轮常常不到 1%，一位小数会看起来不动），10% 及以上一位小数，更小的显示 `<0.01%`，真正的零显示 `0%`。

### 声明文件（未内置的 provider）

`~/.dsh-tui/dsh-peak-balance-providers.json`（可安全删除；坏了就当作空配置）：

```jsonc
{
  "version": 1,
  "apiBases": { "my-relay": "https://relay.example.com" },
  "allowUnofficial": false,
  "providers": {
    "my-relay": {
      "adapter": "declared",
      "auth": { "kind": "bearer", "apiKeyEnv": "MY_RELAY_KEY" },
      "requests": [
        {
          "path": "/api/user/self",
          "headers": { "New-Api-User": "1" },
          "meters": [
            { "id": "balance", "kind": "money", "currency": "USD", "value": "data.quota", "scale": 0.000002 },
            { "id": "periodSpend", "kind": "money", "currency": "USD", "used": "data.used_quota", "scale": 0.000002 }
          ]
        }
      ],
      "spendCounter": "data.used_quota",
      "spendUnit": { "kind": "money", "currency": "USD" }
    }
  }
}
```

点路径支持数组下标（`data.0.results.0.amount`）；`scale` 用来做单位换算；`resetAt` 支持 `ms` / `s` / `iso` / `remainingMs` / `remainingS`。**New API / One API 的账户接口用的是网页 access token，而不是 `sk-` 中转 key**，所以要在凭据库里另存一份（上例的 `MY_RELAY_KEY`），并给 `allowUnofficial` 或全局的「允许非公开端点」开关打开——那是控制台侧接口。

其他常见写法（同样用声明文件即可，不必改代码）：

| 目标 | 关键字段 |
| --- | --- |
| Anthropic 组织级成本（需 admin key） | `GET /v1/organizations/cost_report`，金额路径 `data.0.results.0.amount`，单位是**分**（`scale: 0.01`），币种 USD |
| MiniMax 编程套餐 | `GET https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains`，`model_remains.0.current_interval_usage_count` / `..._total_count`，重置 `remains_time` 用 `resetUnit: "remainingMs"` |
| 智谱余额 | `GET https://open.bigmodel.cn/api/biz/account/query-customer-account-report`，`data.availableBalance`（CNY） |

## 兼容性

| 项 | 值 |
| --- | --- |
| 宿主 | `@deepseek-harness-tui/dsh-tui` 0.10.x（`ctx.tuiStatus.registerView`、`ctx.tuiSettingsSections.register`、`ctx.tuiScenes.register/open`、`ctx.commands.register`、`ctx.tuiCommandTrees.register`、`ctx.tuiShortcuts.register`） |
| Harness | `@deepseek-ai/dsh` 0.1.2-rc.1+（`session/event`、`settings`、`credentials`） |
| 运行时 | Node `^22.19 \|\| >=24`，纯 ESM，无原生依赖（多帧 zstd 用内建 `node:zlib`） |
| Manifest | `manifestVersion` 0.15 · id `com.dsh-tui-ecosystem.dsh-peak-balance` · 契约 `tui.dsh/v1alpha1#DecisionEvents`（optional）+ `commands.dsh/v1alpha1#Command`（required）· 四条命令贡献（`/hist`、`/th`、`/tokenhistory`、`/quota`） |
| 平台 | dsh-tui 能跑的平台（Windows / macOS / Linux） |

所有宿主接缝都是软探测（`ctx.get(name, false)`）：缺少 TUI 扩展服务、缺少 credentials 服务或没有网络时，插件保持静默而不是让宿主失败；注册会在 profile 组合期间重试 30 秒，所有定时器都由 activation 的 effect 清理。

命令注册走**中介面优先**：`ctx.tuiPluginHost.registerCommand`（带已校验的组件身份与调用检查点）→ 拒绝时回退到 `ctx.commands.register`（生态文档里的 C-070 边界）。以普通 profile 行加载的第三方插件没有 verified identity，所以实际生效的是回退路径；两条路径都失败时只记一条日志，插件其余部分照常工作。

`alt+h` 走 `ctx.tuiShortcuts.register`（`ctrl`/`alt` 必需、保留组合会被宿主拒绝并返回 no-op）：宿主只接受非保留组合，`alt+h` 未被 `alt+v`/`alt+up` 等内置键占用。

**语言**：插件按宿主正在显示的语言渲染，解析顺序与 dsh-tui 一致 —— `DSH_TUI_LANG` → 运行时 `dsh-tui.lang` 设置 → `~/.dsh-tui/lang.json` → 系统 locale（locale 缺失时沿用历史默认 `zh`；存在但不支持的语言一律回落到 `en`，与 dsh-tui 相同）。`/lang` 切换通过设置服务的 `settings/updated(ns, next, prev, source)` 事件**即时**重绘状态行与已打开的历史场景；宿主没有提供该命名空间时，退化为每秒轮询持久化文件（用 mtime/size 指纹判断，没变就只做一次 `stat`）。`DSH_PEAK_BALANCE_LANG_FILE` 可覆盖该文件路径，供测试与诊断使用。

## 已知限制

- **输入框边框本身无法由插件改色**：dsh-tui 0.10 的输入框边框由其内部 `EffortInputBorder` 组件独占渲染，没有对插件开放的接缝。因此警告边框是渲染在对话栏正上方的状态贡献——这是不改宿主源码能做到的最接近效果。
- 花费为基于 token 用量的估算，实际扣费以 DeepSeek 平台账单为准。
- 价目表内置在包内，官方调价需要插件更新；未收录的模型需要你自己用 `/hist price set` 补单价。
- 账户那一段依赖 provider 自己的接口：**没有公开额度接口的 provider 不会显示任何数字**（OpenAI、Gemini、Anthropic 预付费余额、GLM/Kimi 编程套餐、Claude 订阅等），而不是显示一个猜出来的值。可以用声明文件接入自家部署或未内置的接口。
- **恢复的对话在发出第一个请求之前，账户数字来自宿主的 `/model` 路由，而不是这个对话本身**：dsh-tui 重放已恢复会话的历史时不发会话事件，所以还没有任何东西报出这个对话自己的 provider。若该对话被单独固定到与 `/model` 选择不同的 provider，它的第一个请求会把状态行纠正过来。
- **全新环境、还没发出任何请求时会落到 `deepseek-official` 路由**：既没有 `~/.dsh-tui/model.json` 偏好、也还没有任何会话事件时，宿主只报得出这一条路由。所以你若打算用别家 provider、又没配 DeepSeek 密钥，账户那一段会先显示「未配置密钥」，直到你的第一个请求报出真正的 provider——不会出错，那个请求之后状态行自己会纠正。
- **只有 DeepSeek 官方与 Command Code 两个适配器经过真实账户核对**（见上文表格）；其余具名适配器按各自官方文档实现，并用文档/源码里的夹具 payload 测试，未经真实账户核对。
- **非公开端点默认关闭**：spec 里声明 `"allowUnofficial": true` 的 provider，只有在全局的 `allowUnofficialQuota` 也打开时才会被访问（如逆向的控制台接口）。Command Code 的 `/alpha/*` 不受该开关限制——那是官方 CLI 自己走的端点。
- **不内置 Command Code 的价目表**：它的模型目录不带价格、价格页是前端渲染的，编一份出来就是猜。订阅套餐里的模型在 `/hist` 只显示 token，直到你用 `/hist price set commandcode:<model> …` 给出单价；状态行的「本轮」仍然是实测扣减。
- **`/hist` 的金额一律是「元」**：成本由价目表算出（内置价目或 `/th price` 自定义费率，单位都是元/百万 token），所以合计行、按 provider 分列与模型表都按 `¥` 显示。`credits` 是状态行上账户自己的额度口径，不参与成本列。
- **历史缓存结构升级会被就地迁移，首次扫描仍然要几秒**：记录结构变化时插件用迁移函数改写旧缓存（不重扫日志），无迁移路径的旧版本才整份丢弃——0.5.1 的那次丢弃就是「升级后第一次 `/hist` 要等十几秒」的来源，现在不会再有。**真正慢的只有本机第一次全量扫描**（本机 437 份 / 112 MB 约 5~6 秒；其中约 4.3 秒是 19 万个小 zstd 帧逐帧解码的固有开销），期间边扫边落盘、边扫边出图：最新的日志先扫，不到一秒就能看到最近数周的真实数据。
- **新 fork 的会话日志在只有 2 帧时无法做帧级续读**：这类日志的第 1 帧是会话头、第 2 帧才是 `session/end-seed` 截断标记，只看到第 1 帧时无从确定截断点，插件会选择整份重读（代价以百字节计）。
- **`/quota` 的 provider 列表**来自 `ctx.llm.listConfigurableProviders()`、内置目录快照与声明文件；组合里声明但宿主当前不可路由的 provider 可能不在列表里。
- **余额按接口回报的币种显示**：接口在 `currency` 里给出币种（`CNY` / `USD` …），状态行使用对应符号（`¥` / `$`）；未知币种回退成 ISO 代码（如 `12.34 CHF`），不会把美元当人民币显示。
- 富状态视图与其他插件共享 6 行预算，本插件占用 3 行，且仅在警告边框显示时占用。
- 状态行显示的是**宿主当前聚焦的那个对话**：切换对话后它跟着切。已结算的轮次按**对话**记录（不是按会话对象），所以切走再切回来仍然能看到那个对话自己的上一轮花费；只有该对话在本进程内确实还没有已结算的轮次时才显示 `本轮 —`（新开的 `/new` 会话就是这种情况），而不是拿别的对话的数字顶上。
- **子代理的花费计入「本轮」**：子代理会话（`origin: subagent` / `delegationDepth > 0`）的用量会按会话头里的 `parentSession` 折叠回**发起它的那个对话**正在进行的轮次；父轮次结束后才回来的后台子代理不再计入（那一轮已经结算）。宿主没有给出父子关系时按旧行为忽略。注意 `/hist` 的总量与状态行口径不同：历史默认把子代理**单独**统计（可用「计入子代理」开关切换），状态行则是把它并进父轮次。
- **空 marker 是稳定状态，不是反复的 `/new`**：宿主在 `/new` 以及**退出一个不可恢复的会话**时都会把 `~/.dsh-tui/resume.txt` 写成空串，所以插件只在标记**内容或 mtime 变化**时才采用它。早期版本每秒重放一次该读数，会让已经认领状态行的对话被永久静音（显示停在 `本轮 —`）。
- **轮次结束时的瞬时判定**：用量报告按其请求开始时刻（`step/start`）归入峰/谷档；一轮的费率与模型按该轮**第一次上报**时的快照计价，因此轮内换模型或跨 2026-09-14 的 Pro→Flash 换挡不会把整轮改价。结算发生在轮末，只影响没记录到任何上报的极端情况。
- **历史只覆盖本机 dsh 的用量**：日志里没有的调用（网页版、其他客户端、其他机器）不会出现在方格图里。历史起点取决于本机日志文件，现有日志从 2026-09-04 开始。
- **`/th` 单独回车会被宿主的补全菜单抢去切主题**（见上文「命令名与回车行为」）：这是宿主逻辑，插件无法把命令排到补全列表前面；用 `/hist`、`/tokenhistory`、`alt+h` 或 `/th ` 带空格都可以。同理，若日后某个技能名以 `hist` 开头，也会以同样方式抢走 `/hist` 的回车。
- **本机第一次打开 `/hist`（还没有缓存）需要几秒**：全量扫描会话日志（本机 437 份 / 112 MB 约 5~6 秒），期间显示扫描进度，并**边扫边出图**（最新日志先扫，最近数周不到一秒即可见）；扫描会边扫边落盘，中途被打断也能续扫。有缓存之后，打开场景直接由缓存出图。
- **鼠标悬停依赖全屏（alternate screen）**：profile 的 `fullscreen: true` 下可用；inline 模式下请用键盘（`←/→/↑/↓`）选中日期，明细卡同样会显示。
- 方格图的深浅按当前可见区间内非零日期的四分位分级，因此数据增长后同一数值的颜色档位可能变化（与 GitHub 一致）；图例里始终标着当前区间的最大值。
- **终端不够宽时裁周而不是缩格**：宽度放不下完整跨度时只显示最近的若干周，标题栏徽章会写「显示 27/53 周」；`←/→` 挪到边缘时窗口跟着滚一格，选中格不会被藏掉。高度不足时按此优先级逐级降级：模型表 → 明细卡的字段（先砍模型行，再砍命中率/花费/token）→ 总计行；`<12` 行或 `<40` 列时改用兜底视图（最近若干天一行一条 + 一行总计），保证不溢出。

## 开发与验证

```sh
pnpm install --frozen-lockfile
pnpm check:encoding      # 无 UTF-8 BOM / 编码损坏（dsh 崩溃的经典元凶）
pnpm validate:manifest   # 准入形状 + 版本一致性
pnpm test                # node:test 单元测试 + 宿主桩集成测试
pnpm pack:verify         # 入口引用到的模块是否都在 files 内
pnpm verify              # 以上四项依次执行
```

宿主集成探测（无头启动一个一次性 profile，验证宿主是否**接受**注册——单测看不见这一层）：

```sh
node ../../tools/probe-plugin.mjs . --wait 15
```

探测 profile 现在会一并挂载 `scenes` / `plugin-host` / `command-trees` / `extensions`（含 `tuiShortcuts`）四行，因此场景、四条命令与 `alt+h` 快捷键的注册都会被验证；期望输出里能看到 `history scene registered`、四行 `command registered`、`command tree registered roots=4` 与 `shortcut registered alt+h`，退出码 0。

### 校验历史数字（真值校验）

聚合逻辑是纯函数、可单测（`test/history.test.js`），但**数据本身**要看真实日志。想在同机复核：

1. 删除缓存 `~/.dsh-tui/dsh-peak-balance-history.json`，让下一次 `/th` 全量重扫；
2. 用一个**独立实现**（不 import 本包的模块）按同一份字节折叠一遍，比较逐日/逐模型数字；
3. 对比时注意会话日志是**活文件**：一边跑一边写会让总数持续增长，两次快照不可能相等，只有工具与插件对**同一批字节**的一致才有意义。

本仓库开发时用这种方式核对过：341 份日志、116 个 `(模型, 日期, 峰/谷)` 桶，两份实现 **0 处不一致**；同时验证了 0 个重复 seq、0 个乱序 seq、0 行非法 JSON。

### 预览峰时警告效果

警告边框只在真实高峰时段（周一至周五 `09:00-12:00` / `14:00-18:00` 北京时间）出现。想在任意时间预览：

```sh
DSH_PEAK_BALANCE_FORCE_PEAK=1 dsh --profile dsh-tui   # PowerShell: $env:DSH_PEAK_BALANCE_FORCE_PEAK=1
```

该开关只改变呈现方式（倒计时仍按真实时钟显示），不设置该变量时完全不生效。

### 诊断日志

插件在 `~/.dsh-tui/dsh-peak-balance.log` 保留一份有上限的生命周期日志：模块被导入一行、`apply()` 开始一行（含 pid 与实际加载的文件路径）、解析出的配置、可挂载的宿主接缝探测结果、每次注册的结果，以及卸载。有了它就能区分「宿主压根没加载这个文件」和「某个接缝拒绝了注册」，不必给运行中的 TUI 挂调试器。文件超过 128 KiB 时自动裁剪保留最新一半；`DSH_TUI_DEBUG=1` 会追加每次刷新的细节。跑测试时不会写入该文件。

聚焦对话来自两条独立信号，按优先级：

1. 宿主托管的 `tui/session-switched` 决策事件通知——宿主挂载了 plugin-interop 行时可用（日志里 `host=1`）。manifest 把该契约声明为 **optional** 并写清兜底方案，缺少它的宿主只会降级，不会被拒绝准入。
2. 宿主每次切换都会重写的启动器标记 `~/.dsh-tui/resume.txt`，每秒读一次。宿主开始新对话（`/new`）时会把这个标记**清空**——这是「有信息」而不是「读不到」：状态行会放掉原来那个数字，改由第一个"不是刚离开的那个对话"来接手。只有**压根读不到**标记时，才回退到「最后一次会话事件」。

`DSH_PEAK_BALANCE_FOCUS_FILE` 可覆盖该标记路径，供测试与诊断使用——测试运行因此永远不会去读真实标记。

## 发布与版本

仓库 [VviLliAm-qwq/dsh-peak-balance](https://github.com/VviLliAm-qwq/dsh-peak-balance)，MIT 许可。版本遵循 SemVer；npm 的 `version` 与 manifest 的 `version` 始终保持一致，打一个与该版本同号的 `v*` tag 即触发发布工作流；该工作流经 npm **可信发布（OIDC）**上传，仓库内不存放令牌。

## 收录

本插件收录在 dsh-tui 插件市场。市场只做链接罗列，**不做代码审查、不代表背书**。

## 给插件作者的坑（本插件踩过的）

- **Cordis 插件入口只能导出 `name`、`Config`、`apply` 三个符号。** 若在同一个模块里额外导出工具函数，宿主对 activation 的包装方式会改变，随后所有 `tuiStatus` / `tuiSettingsSections` 注册都会被拒（`requires a live Cordis activation context`）。这个故障是「半死」的：设置**命名空间**仍会注册成功，于是插件看起来活着，但设置卡片和状态行永远不出现。请把实现放在同目录的另一个模块，入口只做再导出。
- **可选宿主服务要「严格优先」获取**（`ctx.get(name)`）；非严格的 `ctx.get(name, false)` 可能返回影子占位实例，宿主会拒绝其方法调用。非严格形式只作兜底，并且要持续重试 —— 第一拍时接缝行可能仍在激活中。
- **中介式命令注册需要「已校验的组件身份」，普通 profile 行拿不到。** `ctx.tuiPluginHost.registerCommand` 会先 `requireComponentIdentity`，以 `file:` 装进 profile 的第三方插件不是通过宿主的 admission 通道加载的，于是它会抛 `the calling activation has no verified dsh-plugin.json Component identity`。manifest 里照样要如实声明 `commands.dsh/v1alpha1#Command` 与该命令的 contribution id（将来被 admission 接管时才有意义），但代码必须准备好回退到 `ctx.commands.register`，否则命令会静默消失。
- **状态行与全屏场景是两种不同的接缝。** `tuiStatus.registerView` 的富视图**上限 3 行**且拿不到滚轮/键盘；要画多行界面（例如方格图）必须用 `ctx.tuiScenes.register` + `open`，它给的是完整 `ui` 套件（Box/Text/useInput/useTerminalSize/useTheme）和 `close()`。
- **场景里 hooks 必须无条件、按固定顺序调用。** 看起来无害的「`ui.useTheme` 不存在就跳过」会改变 hook 顺序，真实 React 会当场抛 invalid-hook-call。本插件的写法是先取出 `useInput`/`useTerminalSize`/`useTheme`（缺失时给一个返回默认值的桩），再无条件调用。
- **命令名不要是内置命令的前缀。** 宿主的斜杠补全菜单在打开时接管回车，执行的是高亮项，且内置命令排在插件命令之前 —— 名字撞前缀（`/th` vs `theme`/`thinking`）就意味着单独回车永远轮不到你。要么换个不冲突的名字（`/hist`），要么另给一个快捷键（`ctx.tuiShortcuts`，需要 ctrl/alt 且避开保留组合）。
- **全屏场景要自己预算行高。** alternate screen 里溢出不是「被裁剪」而是把整帧推走；每个区块都该先问「还剩几行」，再决定画不画。宽度同理：先算能放几列，再去裁内容，而不是指望宿主截断。

本插件把排查过程写进 `~/.dsh-tui/dsh-peak-balance.log`，上面几条就是这样查出来的（见上文「诊断日志」）。

## 许可

MIT — 见 [LICENSE](LICENSE)。
