Skip to content
dsh.fish
Bundle

dsh-model-surplus

DeepSeek Harness plugin: shows model usage and account balance in the session header — OpenCode Go plan usage (rolling / weekly / monthly) with the selected model's monthly cap, and DeepSeek account balance — each only while a matching model is the active selection.

Source
itchenshi
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-model-surplus

[![English](https://img.shields.io/badge/README-English-green)](README.en.md)
[![中文](https://img.shields.io/badge/README-中文-blue)](README.md)

在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**会话标题右侧**显示
**当前模型的用量 / 账户余额** —— 用到哪个模型,就显示哪一份,不用切页面去查。

```
┌─ 会话标题栏 ───────────────────────────────────────────────────────────────┐
│  我的对话     [OpenCode Go 滚动 18% 周 82% 月 42% 上限 $60]        打开功能 ▾ │
│  另一个对话   [DeepSeek ¥110.00]                                   打开功能 ▾ │
└────────────────────────────────────────────────────────────────────────────┘
```

| 当前模型路由 | 显示什么 | 数据从哪来 |
|---|---|---|
| `opencode-go` / `opencode` | 套餐用量:滚动 / 周 / 月**百分比** + 重置时间,外加**所选模型**的月度上限(`上限 $60`) | `GET https://opencode.ai/zen/go/v1/usage` + 单模型上限表 |
| `deepseek-official` | 账户**余额**:总 / 赠送 / 充值(余额不足时显示「余额不足」) | `GET https://api.deepseek.com/user/balance` |

只有当会话选中的模型属于受跟踪的 provider 时它才出现 —— 切模型就立刻显示/隐藏,不用刷新。

## 装上就能用

- **用 DSH Ready GUI(推荐)**:插件**随 GUI 内置**。打开 GUI → 设置窗口 → 第三方插件 → 勾选
  **模型余量**,装完按提示重启引擎、按提示刷新页面(它带页面部分)。
- **其它 DSH 宿主**(`dsh web` / CLI):

  ```sh
  git clone https://github.com/itchenshi/dsh-model-surplus.git
  dsh plugin --profile web add file:<clone 出来的绝对路径>
  ```

  装到的是磁盘上的真实目录,所以以后 `git pull` 更新的就是同一份代码;反过来,**目录被移动或删掉
  会让这条依赖失效**(重新 add 一次即可)。

> npm 上暂时没有这个包:注册账号那一环走不通(`www.npmjs.com` 返回 Cloudflare 托管挑战),包发不
> 出去。所以现在只能按上面两种方式装。等注册通了会照常发布。

**要配的只有密钥**,插件通过引擎的凭据服务按引用读取,**密钥不会下发到浏览器**:

| 路由 | 凭据名 |
|---|---|
| OpenCode Go | `OPENCODE_GO_API_KEY` |
| DeepSeek | `DEEPSEEK_API_KEY` |

两个分区**各自独立**上报结果:只配了其中一个密钥,另一半照样能用,缺的那半显示一条原因
(`no-key` / `unauthorized` / `network` / `timeout` / `bad-payload`),而不是把整个小组件藏起来。

## 单模型月度上限是怎么来的

OpenCode Go 的用量接口**按账户统计**,忽略单模型参数;而[官方「使用限制」](https://opencode.ai/docs/zh-cn/go/#%E4%BD%BF%E7%94%A8%E9%99%90%E5%88%B6)
里写的单模型月度上限**没有 API**(例如 DeepSeek V4.1 Flash $60/月、DeepSeek V4 Pro $15/月)。所以:

1. **内置一张表**(`BUILTIN_MODEL_LIMITS`),
2. **自动刷新**:抓公开文档页(服务端渲染、不需要 JS、**不需要密钥**),把限制表和模型 id 表解析成
   `模型 id → 月度 $`,
3. 结果**缓存**在 `$DSH_HOME/logs/model-surplus-limits.json`,重启不必重抓,
4. 任何抓取/解析失败都**回退**:缓存 → 内置表。

刷新时机:插件启动时一次,之后每 24 小时一次。tooltip 会列出推导出的各档上限(5h = 20% · 周 = 50% ·
月 = 100%)、数据来源(文档 / 本地缓存 / 内置表),并说明百分比是账户级的、上限是单模型的。

## 配置(`cordis.patch.yml` 的行 config,全部可选)

```yaml
- insert:
    - id: model-usage
      name: dsh-model-surplus
      config:
        enabled: true

        opencodeGo:
          baseUrl: https://opencode.ai/zen/go/v1   # 上游根地址
          apiKeyRef: OPENCODE_GO_API_KEY           # 凭据引用
          providers: [opencode-go, opencode]       # 视为 OpenCode Go 的路由

        deepseek:
          baseUrl: https://api.deepseek.com        # 上游根地址
          apiKeyRef: DEEPSEEK_API_KEY              # 凭据引用
          providers: [deepseek-official]           # 引擎的 DeepSeek 路由
```

改 `providers` 就能把别的路由划进某个分区,**不用改页面代码**——供应商到分区的映射由宿主端每次
轮询时下发。顶层直接写 `baseUrl` / `apiKeyRef` / `providers` 仍按 `opencodeGo` 分区读(本插件早期
只有 OpenCode Go)。

## 权限与边界(给会静态扫描的商城看的)

会锁定 commit 审查的商城会静态扫描运行时代码,这里把事实一次写清:

- **运行依赖:无。** 只用 Node 内建模块(`node:path`、`node:fs/promises`);宿主半边是纯 ESM,
  自身不要求 `node_modules`。
- **对外网络:有,三个主机**,全部来自**宿主端**(页面端只调下面那条本地路由):
  - `GET https://opencode.ai/zen/go/v1/usage` —— OpenCode Go 套餐用量(`OPENCODE_GO_API_KEY`)
  - `GET https://api.deepseek.com/user/balance` —— DeepSeek 账户余额(`DEEPSEEK_API_KEY`)
  - `GET https://opencode.ai/docs/zh-cn/go/` —— 文档里的单模型月度上限;之所以要抓页面,是因为网关
    自己的 `/models` 响应不带任何上限信息
- **本机路由:一条。** 宿主注册唯一一条面向页面的路由,页面以同源方式调用;它走引擎自己的信任围栏
  (Host 白名单 + 浏览器会话 cookie),围栏拿不到时**失败即关闭**。**不带 cookie 的裸 `curl` 会得到
  401**(修复前返回 200 —— 任何本机进程、被 DNS rebinding 的页面都能读到你的用量和余额)。
- **文件:一个缓存。** `<DSH_HOME>/logs/model-surplus-limits.json` 存抓到的上限表,其余不读不写,
  也从不碰你的文件。
- **凭据:** 只在宿主端经 `ctx.credentials` 读取,绝不出现在任何返回给页面的响应里。
- **命令 / 原生制品 / 生命周期脚本:无。**
- **失败边界:** 两个分区各自独立上报,缺密钥或上游不可达只会让那一半降级并说明原因;插件从不阻塞
  引擎启动,卸载即恢复原来的标题栏。

## 两部分构成

| 部分 | 文件 | 运行于 | 职责 |
|---|---|---|---|
| 宿主端 | `lib/index.js` | Node | 按分区抓上游数据,经一条同源 JSON 路由提供出去 |
| 页面端 | `client/client.js` | 浏览器 | 注册标题栏小组件;轮询宿主路由;按当前模型门控 |

宿主路由的返回形状(手工调试用;记得带引擎的会话 cookie):

```
GET /model-usage
{
  "ok": true,
  "sections": { "opencode-go": { "providers": ["opencode-go","opencode"], "keyRef": "OPENCODE_GO_API_KEY" },
                "deepseek":    { "providers": ["deepseek-official"],    "keyRef": "DEEPSEEK_API_KEY" } },
  "opencode-go": { "ok": true, "usage":   { "rolling": {…}, "weekly": {…}, "monthly": {…} } },
  "deepseek":    { "ok": true, "balance": { "isAvailable": true, "infos": [ { "currency":"CNY", "total":"110.00", … } ] } },
  "limits": { "deepseek-v4.1-flash": { "hours5": 12, "weekly": 30, "monthly": 60 } }
}
```

宿主会归一化并钳制用量百分比(0–100),也接受上游发来的十进制**字符串**金额;每个分区缓存 60 秒,
失败后最快 30 秒才重试。

## 开发

```sh
node --check lib/index.js
npm test        # 本地行为测试(不联网、不需要引擎)
```

`tests/e2e.cjs` 是针对真实引擎 + 浏览器的完整集成检查(需要跑着的 profile,所以不在 `npm test` 里)。

## 许可

MIT

Install

dsh plugin --profile web add github:itchenshi/dsh-model-surplus#612438e794524adbb75d937d7d80236e01787331

Profile: web

Source