# dsh-deepseek-usage-monitor [English](README.md) | 简体中文 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![test](https://github.com/KamChiHei/dsh-usage-monitor/actions/workflows/test.yml/badge.svg)](https://github.com/KamChiHei/dsh-usage-monitor/actions/workflows/test.yml) [![npm version](https://img.shields.io/npm/v/dsh-deepseek-usage-monitor)](https://www.npmjs.com/package/dsh-deepseek-usage-monitor) [![npm downloads](https://img.shields.io/npm/dm/dsh-deepseek-usage-monitor)](https://www.npmjs.com/package/dsh-deepseek-usage-monitor) [![GitHub stars](https://img.shields.io/github/stars/KamChiHei/dsh-usage-monitor)](https://github.com/KamChiHei/dsh-usage-monitor/stargazers) DeepSeek Harness(`dsh`)插件:在 Host 侧自动记录每次模型调用的 token 用量,定时查询 DeepSeek 账户余额,并在 DSH Web 右下角显示一张可拖动、可调整大小的实时状态卡。 插件分为两半,读的是同一份数据: - **Host 侧**(`index.js`):监听 Harness 事件完成记账,定时查询余额,提供状态接口; - **Web 侧**(`client.js`,经 `package.json` 的 `dsh.client` 声明加载):轮询状态接口,渲染右下角「用量」卡片。API key 始终留在 Host 进程,不会发到浏览器。 ## 展示 安装后 DSH Web 右下角的「用量」卡片(图中为展开状态,含总 token、缓存命中率、余额与模型 / Provider 分组): ![DSH Web 右下角展开的「用量」状态卡](docs/screenshot.png) ## 功能 ### Token 记账 - 监听 `session/event`:以 `assistant/message` 的 `TokenUsage` 为准入账;`assistant/chunk`(`chunk.type === "usage"`)记录的 usage 作为失败请求的兜底来源,并以 `会话:turn:step` 为键去重,同一步骤不会重复统计;`step/end` 和 `session/disposed` 会把始终没有得到 message 确认的 chunk usage 补记入账。 - 兼容两种 usage 字段:Harness 的 `inputTokens / outputTokens / cacheReadTokens / cacheWriteTokens`,以及 DeepSeek 原始响应的 `prompt_tokens / prompt_cache_hit_tokens / prompt_cache_miss_tokens / completion_tokens ...`(自动换算,缺省时 miss = prompt − hit)。 - `totalTokens = 输入 + 输出 + 缓存读 + 缓存写`;reasoning token 已包含在输出里,单独累计但不会重复相加。 - 除总账外,还按**模型**、**Provider** 两个维度分组累计;会话明细按最后请求时间保留最近 `sessionLimit` 个(`sessionCount` 为当前保留的会话数)。路由信息来自 `request/header` / `request/context` 事件,缺失时归入 `unknown` 分组。 - 统计持久化为本地 JSON(默认 `~/.deepseek-harness/deepseek-usage.json`),重启后继续累计。只保存数字、分组名和时间戳,不保存 API key、提示词或模型回复;想清零统计,删除该文件后重启 DSH 即可。 ### 余额查询 - 定时(默认 60 秒)调用 DeepSeek 官方 `GET /user/balance`,记录 `is_available` 与 `balance_infos` 金额;请求超时(默认 10 秒)或失败会记录原因。 - API key **按次解析**,自动复用 dsh 已配置的 DeepSeek key(解析顺序见「API key」);启动后才补配的 key,下一次余额刷新自动生效,无需重启。 - 后台定时刷新在解析不到 key 时静默跳过(卡片显示「未查询」);手动点「刷新」才会标记「查询失败」,悬停余额一栏可看到具体原因(包括 key 未配置的诊断信息)。token 统计不依赖 key,始终正常工作。 ### 状态接口 `GET /plugins/deepseek-usage-monitor/state`:网页卡片使用的状态接口;加 `?refresh=1` 强制刷新余额;也支持 `HEAD`。返回结构见下方「状态接口返回结构」。 ### DSH Web 状态卡 安装后 DSH Web 右下角出现「用量」卡片,每 5 秒自动拉取一次状态(页面在后台时暂停轮询,回到前台立即刷新一次): - 展开可见:总 Token、请求数、缓存命中率、输入(未命中缓存)、输出 token、DeepSeek API 余额、模型 / Provider 分组列表和更新时间;点「刷新」立即强制刷新余额(等价于 `?refresh=1`)。 - 缓存命中率 = 缓存读 /(缓存读 + 未命中输入)。 - 模型 / Provider 分组按总 token 降序展示,默认只显示前 4 项,点「显示全部 N 项」展开、「收起」折叠;无数据时显示「暂无数据」。 - 余额一栏的状态:正在读取… / 金额(多币种以 `·` 连接)/ 暂无余额 / 不可用 / 未查询 / 查询失败(悬停显示原因)。 - 默认收起为一条标题栏,点「+」展开、「−」收起,展开/收起状态会被记住。 - 按住标题栏拖动移动位置,拖动右下角把手调整宽高(最小 232×96),双击标题栏复位到默认右下角锚点;位置、尺寸和收起状态保存在浏览器 localStorage(键 `dsh-deepseek-usage-monitor:placement`),刷新页面后保持。 - 收起时自动隐藏缩放把手并回到标题栏的停靠点;在屏幕边缘展开或窗口缩小时,卡片会自动收回视口内。 - 状态点在状态接口读取失败时变红,错误信息显示在卡片底部。 - 样式基于 DSH 官方设计令牌(`--dsw-*` 负责背景、边框、文字层级与状态色,`--ds-*` 负责动效),自动适配深色/浅色主题,并带有回退值;小屏(≤560px)自适应宽度。 - 卡片界面语言跟随浏览器语言:`zh` 开头的语言环境显示中文,其他显示英文。 ## 环境要求 - Node.js ≥ 22.19 - pnpm(`dsh plugin` 本质是在 profile 目录里转发 pnpm) - 不需要全局安装 `dsh`:所有 `dsh` 命令都可以用 `pnpm dlx` 运行,本文统一写作: ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 <命令> ``` 把 `0.1.1-rc.2` 换成你实际使用的 dsh 版本即可(`package.json` 的脚本也是这样写的)。 ## 安装到 profile Harness 的配置与 profile 存放在 `~/.dsh`(Windows 上是 `C:\Users\<你>\.dsh`),web profile 位于 `~/.dsh/profiles/web`。`dsh plugin` 会在该目录里转发 pnpm,并把声明了 `dsh.bundle` 的依赖自动加入 profile 的 bundle 层——不需要手改任何 YAML。 ### 方式一:npm 安装(推荐,稳定版) 不需要克隆仓库,也不需要手动安装依赖,在任意目录执行: ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-deepseek-usage-monitor ``` - 插件依赖(`@deepseek-ai/schemastery` 等)会装进 profile 自身的 `node_modules`,无需其他步骤,并自动加入 `dsh.profile.bundles`; - 更新到最新版:重新执行同一条命令即可; - 锁定特定版本:`plugin --profile web add dsh-deepseek-usage-monitor@0.1.0`。 ### 方式二:GitHub 直装(追踪最新提交) 安装源直接指向 GitHub 仓库,拿到的是 main 分支最新代码: ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add github:KamChiHei/dsh-usage-monitor ``` - `~/.dsh/profiles/web/package.json` 中会出现 `"dsh-deepseek-usage-monitor": "git+https://github.com/KamChiHei/dsh-usage-monitor.git"`,并自动加入 `dsh.profile.bundles`; - 更新到最新提交:重新执行同一条命令; - 锁定特定版本:把安装源换成 `github:KamChiHei/dsh-usage-monitor#v0.1.0` 这样的 tag 引用。 ### 方式三:本地 link 安装(需要改源码时) 在插件目录中执行两步: ```powershell cd C:\path\to\dsh-usage-monitor # 1. 安装插件自身的依赖(必须先做,见下方说明) pnpm install # 2. 把插件注册到 web profile pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add . ``` 也可以直接用本仓库自带的一键脚本(在插件目录内运行,效果同上第 2 步): ```powershell pnpm run install:web ``` **为什么要先 `pnpm install`**:pnpm 会把本地目录注册为 `link:` 依赖(符号链接),不会替插件目录安装 `@deepseek-ai/schemastery` 等依赖;Node 从插件的真实路径解析模块,也不会经过 profile 的 `node_modules`,所以插件目录必须有自己的 `node_modules`。 安装成功后: - `~/.dsh/profiles/web/package.json` 会多出 `"dsh-deepseek-usage-monitor": "link:C:/path/to/dsh-usage-monitor"`,并自动加入 `dsh.profile.bundles`; - 由于是 `link:` 活链接,修改插件源码后**重启 DSH 即生效**,无需重新安装。 ### 启动与验证 启动(和平时一样): ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 web ``` Host 启动日志里应能看到 `[deepseek-usage-monitor] loaded; web: /plugins/deepseek-usage-monitor/state`,右下角出现「用量」卡片即安装成功。 检查插件层是否进入组合后的配置树: ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config ``` 输出中应能看到 `# == dsh-deepseek-usage-monitor` 分层。 如果你使用其他 profile,把 `web` 换成对应的 profile 名称。 ### 移动插件目录后要修复 `node_modules` pnpm 在 `node_modules/@deepseek-ai/` 下生成的是**绝对路径符号链接**。插件目录一旦移动或重命名,这些链接会全部悬空,dsh 启动时报 `Cannot find package '@deepseek-ai/schemastery'`,且普通的 `pnpm install`(Already up to date)不会修复。此时在插件目录执行: ```powershell Remove-Item -Recurse -Force node_modules pnpm install ``` ### 卸载 ```powershell pnpm run uninstall:web # 或 pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web remove dsh-deepseek-usage-monitor ``` ## 本地源码调试 官方基础教程的 `--patch` 方式需要把插件入口写成绝对路径。本目录提供了模板 `cordis.local.patch.yml`,其中 `index.js` 的路径是写死的绝对路径,克隆本仓库或移动目录后,请先改成你本地的实际路径。 从任意目录(通常是插件目录本身)运行: ```powershell pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 web --patch "C:\path\to\dsh-usage-monitor\cordis.local.patch.yml" ``` 或在插件目录内直接用一键脚本(相对路径按当前目录解析): ```powershell pnpm run dev:web ``` `--patch` 方式加载的是源码入口,同样依赖插件目录里已执行过 `pnpm install`。 ## API key 余额查询需要 DeepSeek API key,但**通常不需要额外配置**:插件会自动复用 dsh 已配置的 key——也就是网页 Models 页写入的凭据存储(`~/.dsh/.credentials.yaml`)。只要你在 dsh 里能用 DeepSeek 模型对话,余额查询就能直接工作。 key 按下述顺序解析,高优先级命中即停止: 1. 插件配置 `apiKey`(见下表); 2. 启动环境变量 `DEEPSEEK_API_KEY`(在启动 `dsh web` 的同一个终端里 `$env:DEEPSEEK_API_KEY = "sk-..."` 后再启动;这两项在启动时固定); 3. dsh 凭据服务(`ctx.get("credentials")`,**每次刷新时重新解析**),依次覆盖:进程环境变量 → Models 页凭据存储 → 项目 `.env` → `~/.dsh/.env`。 启动后才在 Models 页补配的 key,下一次余额刷新(间隔见 `balanceRefreshMs`)自动生效,无需重启;而前两项(配置和启动环境变量)在启动后修改则需要重启。 ## 配置 可在 profile 的 `cordis.patch.yml`(`~/.dsh/profiles/web/cordis.patch.yml`)中覆盖配置。由于 DSH patch 是整行替换,覆盖时要保留 `name`: ```yaml - replace: - id: deepseek-usage-monitor name: dsh-deepseek-usage-monitor config: balanceRefreshMs: 60000 requestTimeoutMs: 10000 recentLimit: 200 ``` 可配置项: | 配置 | 默认值 | 作用 | | --- | ---: | --- | | `apiKey` | `""`(空) | 显式指定的 DeepSeek API key,优先于环境变量与 dsh 凭据存储;留空则自动复用 dsh 已配置的 key | | `baseUrl` | `https://api.deepseek.com` | DeepSeek API 地址(末尾斜杠会被去掉) | | `storePath` | `~/.deepseek-harness/deepseek-usage.json` | 统计文件路径(支持 `~` 展开) | | `balanceRefreshMs` | `60000` | 余额刷新间隔(实际不小于 5000) | | `requestTimeoutMs` | `10000` | 余额请求超时(实际不小于 1000) | | `recentLimit` | `100` | 保留并在状态接口返回的最近调用数(实际不小于 1) | | `sessionLimit` | `50` | 按最后请求时间保留的最近会话数(实际不小于 1) | ## 使用 安装并重启 DSH Web 后,右下角的「用量」卡片会自动工作,不需要任何对话操作;卡片的具体交互见上方「DSH Web 状态卡」。点「刷新」可立即强制刷新余额(等价于 `?refresh=1`)。 ### 状态接口返回结构 `GET /plugins/deepseek-usage-monitor/state` 返回如下结构: ```json { "generatedAt": "2026-08-22T00:00:00.000Z", "totals": { "requests": 15, "inputTokens": 21000, "outputTokens": 8000, "cacheReadTokens": 15000, "cacheWriteTokens": 1200, "reasoningTokens": 4000, "totalTokens": 45200, "lastRequestAt": "2026-08-22T00:00:00.000Z" }, "sessionCount": 2, "models": [ { "key": "deepseek-chat", "totals": { "requests": 12, "totalTokens": 45678 } }, { "key": "deepseek-reasoner", "totals": { "requests": 3, "totalTokens": 12345 } } ], "providers": [ { "key": "deepseek", "totals": { "requests": 15, "totalTokens": 58023 } } ], "balance": { "checkedAt": "2026-08-22T00:00:00.000Z", "isAvailable": true, "balanceInfos": [{ "currency": "CNY", "total_balance": "110.00" }] }, "recent": [{ "timestamp": "…", "sessionId": "…", "turn": 1, "step": 1, "provider": "deepseek", "model": "deepseek-chat", "usage": { "…": "…" } }] } ``` 说明: - `models` / `providers` 按总 token 降序排列(同 token 数按名称排序),`recent` 按时间倒序、最多 `recentLimit` 条,`sessionCount` 为保留的最近会话数(上限 `sessionLimit`); - 余额查询失败时 `balance` 里会出现 `error` 字段(含原因),`isAvailable` 为 `false`; - 分组名缺失时归入 `unknown`;旧版统计文件没有分组数据时会自动从空分组开始,无需迁移。 ## 验证与测试 不需要真实 API key 即可跑测试:纯函数部分覆盖 usage 归一化、reasoning 去重、分组键、分组累计、排序、会话裁剪与路径展开;集成部分覆盖 `UsageLedger` 的记账去重、失败请求兜底、持久化往返、旧统计文件迁移、写盘失败恢复与余额刷新(key 通过 stub 提供): ```powershell pnpm test ``` 检查入口语法: ```powershell node --check index.js ``` 余额结构遵循 DeepSeek 官方的 `is_available` / `balance_infos` 返回值;token 结构遵循 Harness 的 `TokenUsage` 规范和 DeepSeek 的 prompt cache 字段。 ## 项目结构 | 文件 | 作用 | | --- | --- | | `index.js` | Host 侧入口:事件记账、余额刷新与状态接口 | | `client.js` | Web 侧入口:右下角状态卡 UI 与轮询逻辑 | | `usage-utils.mjs` | 纯函数:usage 归一化、累计、分组、排序、会话裁剪与存储路径展开(可独立测试) | | `cordis.patch.yml` | 安装到 profile 时随 `dsh.bundle` 声明的插入项 | | `cordis.local.patch.yml` | `--patch` 源码调试模板(含写死的绝对路径,克隆后需修改) | | `tests/usage-utils.test.mjs` | 纯函数测试(`node --test`) | | `tests/usage-ledger.test.mjs` | `UsageLedger` 集成测试:记账去重、持久化、余额刷新(`node --test`,无需真实 key) |