Bundle
dsh-peak-usage
DeepSeek Harness 插件:输入框下方实时显示本会话消费金额,按官方峰谷分时定价逐请求计价,并读取账户余额
- Source
- Mirfakk
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-peak-usage
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的额度面板插件。装上之后,输入框下面会多出一行实时读数:
```
高峰 | 本次消费 ¥0.1043 | tokens 100.0k↑ / 4.5k↓ | 缓存命中 88% | 余额 ¥18.32
```
四个数字分别是:**当前是高峰还是空闲时段**、**本次会话消费金额**、**本会话 token 用量**、**DeepSeek 账户余额**。
- 金额按 DeepSeek 官方的**峰谷分时定价**计算,逐请求判定时段——不是拿当前时段的价格去乘全部 token。
- 余额从官方 `GET /user/balance` 实时读取,三层机制保证它持续更新。
- 内置官方价格表(含核对日期与来源),装完即可算钱,不需要先手抄一遍单价。
- **零 npm 依赖,零构建步骤。** 安装就是拷文件;`lib/client.js` 手写的就是最终产物。
> 独立第三方插件,面向 DSH 的 `web` profile。验证环境是社区项目 [DeepSeek-Harness-Desktop](https://github.com/web-casa/DeepSeek-Harness-Desktop) 打包的桌面端(v0.2.18)+ 官方 harness `@deepseek-ai/dsh` 0.1.1-rc.2。与 DeepSeek 官方及该桌面端作者均无隶属或背书关系。详见[适用环境与归属](#适用环境与归属)。
```
┌───────────────── 宿主进程(Node) ─────────────────┐
│ ctx.credentials.resolve('DEEPSEEK_API_KEY') │ key 永不出宿主
│ │ │
api.deepseek.com ◄──── GET /user/balance ──► TTL + 单飞 + 节流 │
│ │ │
│ Session.events ──► 按每个样本的 time 折叠峰/谷分桶 │
│ │ │
│ ctx.webServer.register('/plugin/dsh-peak-usage/...') │
└────────────┬───────────────────────────────────────┘
│ 同源 fetch(15s 兜底 + 用量变化即刷)
┌────────────▼───────────────────────────────────────┐
│ 浏览器:conversation.composer.dock 槽位的一行读数 │
│ + useProjection('tokenUsage') ← 框架推送的投影 │
└────────────────────────────────────────────────────┘
```
---
## 适用环境与归属
- 插件面向的是 **DSH(DeepSeek Harness)的 `web` profile**,与具体客户端外壳无关——任何能跑 `dsh web` 的环境都应该能用。
- 开发与验证环境是社区项目 **[web-casa/DeepSeek-Harness-Desktop](https://github.com/web-casa/DeepSeek-Harness-Desktop)** 打包的桌面端 `DSH Desktop` 0.2.18(bundle id `com.yeagoo.dsh-desktop`,Tauri + WKWebView),它内置的 harness 是**官方** `@deepseek-ai/dsh` `0.1.1-rc.2`([deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness))。「桌面端外壳」是社区项目,「harness 内核」是官方项目,两者不是一回事。
- **本插件是独立第三方项目,与 DeepSeek 官方、与该桌面端的作者均无隶属或背书关系。**
- 已测试:harness `0.1.1-rc.2` + DSH Desktop `0.2.18`。
### 兼容性提示
这个插件读的是 harness 的**内部约定**而不是稳定 API,所以 harness 升级后可能需要适配。依赖面如下:
| 依赖 | 用途 | 敏感度 |
| --- | --- | --- |
| `ctx.webServer.register()` | 宿主→浏览器的承载通道 | 低(公开插件契约) |
| `ctx.credentials.resolve()` | 取 API Key | 低 |
| `ctx.timeout` / `ctx.interval` | 余额保活与延迟补刷 | 低 |
| `ctx.sessions.get()` 与 `Session.events` | 读会话日志做峰谷折叠 | 中 |
| `SessionEvent.time`、`assistant/message` 与 `assistant/chunk` 的 usage 形状 | **逐请求**判定峰谷时段 | **高**(最易变) |
| 槽位 `conversation.composer.dock` 及其 `sessionId` / `useProjection` props | 界面挂载点 | 中 |
| 内建投影 `tokenUsage` | 前端实时 token 数 | 中 |
harness 目前是 `-rc` 预发布版本,接口变动属于正常。升级后请跑 `npm test` 与 `bash verify.sh`:测试挂了就说明上面某项变了。
---
## 安装
> **包名与仓库名不同**:npm 包名是 **`dsh-peak-usage`**,GitHub 仓库名是 **`dsh-plugin-usage`**。
> npm 上的 `dsh-plugin-usage` 已被另一个同类插件占用,所以包名加了 `peak`——顺带把「峰谷计价」这个差异化写进了名字。装的时候用包名,读源码用仓库名。
### 方式一:插件市场(推荐)
DSH Desktop 内置了 [cordis.run](https://cordis.run) 插件市场。打开**设置 → 插件**,搜索 `dsh-peak-usage` 即可安装。
命令行等价做法:
```bash
dsh plugin web add dsh-peak-usage
```
### 方式一之补充:从文件安装(sideload,免审核)
想让人**立刻**用上、不等市场审核,就把插件打成 `.tgz` 发给对方,在应用里用「从文件安装」选中它:
```bash
bash pack.sh # 生成 dist/dsh-peak-usage-<版本>.tgz
```
> ⚠️ **必须是 `.tgz`,`.zip` 一定被拒。** 桌面端的原生校验写死了
> `sideload file must end with .tgz`、`sideload path must be absolute`,而且安装动作就是
> `pnpm add <绝对路径>.tgz`。所以别拿 zip 去试。`pack.sh` 两个格式都会生成,zip 只是给人解压看的。
### 方式二:从源码手动安装
插件只改 `$DSH_HOME`(用户数据目录),不碰应用本体。
```bash
git clone https://github.com/Mirfakk/dsh-plugin-usage.git
cd dsh-plugin-usage
bash install.sh
```
`install.sh` 做三件事,且**幂等**(重复跑不会插两次):
1. 把包拷到 `$DSH_HOME/profiles/web/node_modules/dsh-peak-usage`
2. 往 `$DSH_HOME/profiles/web/cordis.patch.yml` 插一行插件条目
3. 跑静态自检(补丁语法 + profile 能否解析到包 + bundle 是否存在)
`DSH_HOME` 的取值顺序:环境变量 → DSH Desktop 的 `~/Library/Application Support/com.yeagoo.dsh-desktop/harness` → 命令行版 dsh 的 `~/.dsh`。也可以显式指定:`DSH_HOME=/path/to/home bash install.sh`。
然后:
1. **重启 DSH Desktop** —— Web profile 里 `hmr` 是被显式禁用的(`dsh-web-app/cordis.patch.yml` 里 `- id: hmr / disabled: true`),所以改 `cordis.patch.yml` **不会热挂载**,必须重启进程。
2. **刷新页面** —— 浏览器半的 bundle 要进 `window.__DSH_BOOT__` 启动图,只在页面加载时装配。
3. `bash verify.sh` —— 在线自检(宿主路由 / boot 图 / bundle 可下载 / 同源闸门)。
---
## 峰谷分时定价
DeepSeek 官方价格页的口径([来源](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)):
> 空闲时段价格为高峰时段价格的一半。北京时间周一至周五(**不含中国法定节假日**)9:00–12:00、14:00–18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段。
内置的官方价表(元 / 百万 tokens,核对于 2026-09-21):
| 模型 | 时段 | 输入·缓存命中 | 输入·未命中 | 输出 |
| --- | --- | --- | --- | --- |
| `deepseek-flash` | 高峰 | 0.04 | 2 | 8 |
| `deepseek-flash` | 空闲 | 0.02 | 1 | 4 |
| `deepseek-v4-pro` | 高峰 | 0.30 | 9.0 | 27.0 |
| `deepseek-v4-pro` | 空闲 | 0.15 | 4.5 | 13.5 |
**为什么金额必须逐请求判定时段。** 空闲价正好是高峰价的一半,所以「拿当前时段的价格乘全部 token」在任何跨时段的会话上都会偏大最多一倍。本插件折叠会话日志时,用的是**每个用量样本自己的 `event.time`**,所以一次跨越 12:00 边界的会话会把两侧分别计价。鼠标悬停在金额上能看到高峰/空闲的分项。
**模型别名是必需的,不是可选的糖。** 官方脚注写明 `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 仍是可调用的旧名,由 DeepSeek-V4.1-Flash 提供服务并**按 Flash 价格计费**。而 harness 的默认模型恰好就叫 `deepseek-v4-flash`,所以插件内置了别名映射(`deepseek-v4-flash` → `deepseek-flash`)。
### 中国法定节假日要自己填
官方节假日由国务院每年公告,代码里算不出来。插件默认清单为空——**后果只是「节假日按工作日规则计价」**,平日的账不会算错。要精确的话,在配置里列出来:
```yaml
pricing:
schedule:
holidays: ['2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',
'2026-10-05', '2026-10-06', '2026-10-07']
```
---
## 余额为什么总是新的
「实时」不能靠一个定时器就算数,所以这里叠了三层:
| 层 | 机制 | 解决的问题 |
| --- | --- | --- |
| ① 后台保活 | 宿主每 `pollIntervalMs`(默认 15s)刷一次 | 即便用户什么都不做,读数也持续前进 |
| ② 用量触发 | 浏览器在 `tokenUsage` 投影变化时立刻请求 `?fresh=1` | 用量变化 ⟺ provider 刚上报样本 ⟺ 钱刚花掉,此刻最该刷新 |
| ③ 延迟补刷 | 强制刷新后 `settleDelayMs`(默认 3s)再刷一次 | 官方账单往往一两秒才扣完,那一刻读到的可能还是扣费前的余额 |
②③ 都可能很频繁,所以宿主侧还有一层 `minRefreshIntervalMs`(默认 3s)节流:**强制刷新保证的是「不晚于这个间隔」,不是「每一次都出网」**。这是保护官方接口不被自己打爆的关键。
读数本身携带新鲜度:每次响应都带 `fetchedAt` / `ageMs`,超过两个保活周期没推进时界面会显示 `余额 ¥18.32 ?`,而不是假装它新鲜。刷新失败时保留上一次成功的读数,并在 tooltip 里说明失败原因。
---
## 配置
编辑 `$DSH_HOME/profiles/web/cordis.patch.yml` 里 `dsh-peak-usage` 那段。Web profile 的 HMR 是关闭的,所以宿主半的配置改动**需要重启进程**才会生效。
| 键 | 默认 | 说明 |
| --- | --- | --- |
| `endpoint` | `/plugin/dsh-peak-usage/summary` | 路由路径,改了要同步改 `lib/client.js` 顶部的 `ENDPOINT` |
| `credentialsRef` | `DEEPSEEK_API_KEY` | 凭证引用名,与 `llm-deepseek` 的 `apiKeyEnv` 一致 |
| `baseURL` | `https://api.deepseek.com` | 官方 API 根地址 |
| `pollIntervalMs` | `15000` | 后台保活周期 |
| `minRefreshIntervalMs` | `3000` | 两次真实出网之间的最小间隔 |
| `settleDelayMs` | `3000` | 强制刷新后延迟补刷的间隔,`0` 表示不补 |
| `requestTimeoutMs` | `8000` | 单次余额请求超时 |
| `pricing.enabled` | `true` | `false` 只显示 token 与余额,不算钱 |
| `pricing.currency` | `CNY` | 价表币种,同时决定取余额响应里的哪个币种 |
| `pricing.fallbackModel` | `deepseek-flash` | 会话模型不在价表里时用哪一行 |
| `pricing.models.<名>.peak/offPeak` | 官方值 | 按「模型 × 时段 × 字段」深合并覆盖 |
| `pricing.schedule.peakWindows` | `['09:00-12:00','14:00-18:00']` | 高峰窗口,北京时间 |
| `pricing.schedule.peakWeekdays` | `[1,2,3,4,5]` | 高峰星期,1=周一…7=周日 |
| `pricing.schedule.holidays` | `[]` | 法定节假日 `YYYY-MM-DD` 清单 |
只覆盖一个字段是完全合法的——其余字段继续取官方值,包括另一个时段:
```yaml
pricing:
models:
deepseek-flash:
peak:
output: 8.5 # 只改这一项;offPeak 与其它字段保持官方值
```
配置写错不会让插件加载失败:非法窗口 / 星期 / 日期都会被丢弃并记进 warning,随读数一起回给前端(悬停在余额或金额上可以看到)。
---
## 自检
```bash
npm test # 等价于 bash test/run.sh
```
> 测试与脚本(`test/`、`install.sh`、`verify.sh`、`publish.sh`)随仓库发布,**不随 npm 包发布**——npm 包装的是运行所需的 6 个文件。要跑测试请克隆仓库。
六个套件、**97 项断言,全部离线**(余额接口打桩,不联网):
| 套件 | 覆盖 |
| --- | --- |
| `test/peak.mjs` | 峰谷判定:窗口边界(左闭右开)、周末、节假日、跨午夜窗口、UTC+8 固定换算(与机器时区无关) |
| `test/pricing.mjs` | 官方价表逐项、空闲价 = 高峰价的一半、模型别名归一、深合并、逐桶计费 |
| `test/fold.mjs` | 日志折叠:同一步的早期/最终样本只计一次、跨时段分类、替换样本跨边界、增量缓存、坏输入 |
| `test/handler.mjs` | 宿主路由契约:含失败路径、同源闸门、新鲜度节流 |
| `test/preflight.mjs` | 浏览器半能否执行、求值、占槽(只 require 白名单模块) |
| `test/render.mjs` | **端到端**:宿主路由的真实响应 → 浏览器半渲染出的那一行读数 |
`test/render.mjs` 是最有用的那一个:它跑真实的宿主处理器拿到 JSON,再喂给打桩的 fetch 让浏览器半渲染,所以「宿主改了字段名但前端没跟上」这类错误一定会被抓住。
---
## 升级时会发生什么
插件由两半组成,而它们各自在不同的时机换版本:
| 半 | 何时换版本 |
| --- | --- |
| 宿主半(`lib/index.js`) | 重启 DSH 进程时 |
| 浏览器半(`lib/client.js`) | 刷新页面时 |
Web profile 的 HMR 是关闭的,所以升级插件后**必须重启进程 + 刷新页面**。为了让这个窗口期不至于变成一句看不懂的 `本次消费 —`,响应里带了一个协议版本号 `protocol`:两边对不上时,那行读数会直接显示「宿主半版本不匹配,请重启 DSH 后刷新页面」。`bash verify.sh` 也会明确报出来。
改动响应结构时,请同时把 `lib/index.js` 的 `PROTOCOL` 与 `lib/client.js` 的 `PROTOCOL` 一起 +1。
---
## 已知限制
- **金额是估算,不是账单。** 它等于「token × 你配置的单价」,官方没有对账接口。实际扣费以官方账单为准。
- **法定节假日需要手工维护。** 见上文。
- **只统计本会话。** 跨会话 / 按天累计需要宿主落盘记账,本版本没有做。
- **冷会话会降级。** 会话未附着到本进程时拿不到 live log,此时金额按「当前时段」粗算并显示 `≈` 前缀;余额不受影响。
- **金额跟着轮询走。** token 数是实时的(走内建投影推送),但金额要等宿主算完——用量变化时会立刻触发重拉,所以实际延迟是一个本地 HTTP 往返,不是 15 秒。
- **HTTP 路由不在 DSH 的 `/api` 浏览器信任围栏内**(那道围栏归 connection 插件所有)。插件自己加了同源闸门:带 `Origin` 的请求必须与 `Host` 同源,`sec-fetch-site: cross-site` 一律 403。服务默认只绑 `127.0.0.1`,且这条路由只读。
- **API Key 永远不进浏览器**,宿主解析后只在该次请求的 `Authorization` 头里用一次。
---
## 为什么不用官方的 Remote 通道
DSH 有正规的 Host↔Client 通道 `ctx.remote.$mount()`,但它的 README 写明 **"Only strict generated contributions can mount on the Client face"**——必须用仓库里 Typert 代码生成出的 descriptor,而且 `dsh-api-remotes` 的能力集是**编译期写死**的(目前只挂了 Goal 与 pluginInventory)。外挂插件挂不上去。
所以宿主半自己注册一条 `ctx.webServer` 路由,浏览器半 `fetch` 同源路径。代价是绕过了 `/api` 信任围栏(用同源闸门补上),换来的是这个插件**不需要 TypeScript、不需要打包器、不需要 node_modules**。
---
## 目录
| 路径 | 作用 |
| --- | --- |
| `lib/peak.js` | 峰谷时段判定(纯函数,UTC+8 固定偏移) |
| `lib/pricing.js` | 官方价表、模型别名、逐桶计费(纯函数) |
| `lib/usage-fold.js` | 把 `Session.events` 折叠成按峰谷分桶的 token(纯函数 + 增量缓存) |
| `lib/index.js` | 宿主半:凭证、余额、模型解析、HTTP 路由 |
| `lib/client.js` | 浏览器半:`window.__ModuleLoader__.load` 注册 + `composer.dock` 槽位组件 |
| `install.sh` / `verify.sh` | 安装 / 在线自检 |
| `test/` | 离线测试 |
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:Mirfakk/dsh-plugin-usage
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-peak-usage from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.