Skip to content
dsh.fish
Bundle

@david0702/dsh-cost

DSH(DeepSeek Harness)对话底部费用显示插件:按每笔请求的时间+模型分批计费,分时段(高峰/空闲/历史)明细,模型归属,读图金额与账户余额。

Source
david0702
License
MIT
Updated
Updated 17 hours ago

Readme

# dsh-cost

DSH(DeepSeek Harness)对话底部的**费用显示插件**。在对话下方的统计行基础上,展示实时累计费用,并把费用按**每笔请求的实际发生时间与当时的模型**分批准确计费,支持高峰 / 空闲 / 历史分时段、模型归属、读图金额与账户余额。

## 功能

- **按笔准确计费**:逐请求按 `发生时间 × 当时模型单价` 累加,而不是拿当前价套全程。
- **分时段计价(三段时间轴)**:
  - 北京时间 **2026-09-10 12:00 之前** —— 归入「历史」,按当时旧价(8-17 前 flat、8-17~9-10 旧峰谷价)计费。
  - **2026-09-10 12:00 起** —— flash 系列降价:空闲时段 输入缓存命中 0.02 / 未命中 1.0 / 输出 4.0 元/M,高峰时段为空闲的 2 倍(0.04 / 2.0 / 8.0)。这一时段再分「高峰 / 低谷」。
  - 高峰 = 工作日 9:00–12:00 / 14:00–18:00(北京时间),其余为空闲。周末一律空闲。
  - 注:V4 Pro 请求后续会路由到 V4.1 Flash 并按 V4.1 Flash 单价计费,本插件在 2026-09-10 12:00 起对其亦按新 flash 价处理。
- **分时段明细卡**(悬停/点开药丸查看):高峰 / 低谷 / 历史 三档,当前档加粗,底部按金额占比分段小条。
- **官方同款「药丸」外观**:底部那一行沿用 DSH 自带统计行(`dsh-client-ui-chat` 的 StatsPills)的规格,
  图标 14px、`border-radius:24px`、`padding:1px 8px`、`gap:6px`、13px 三级文字色、tabular-nums,
  hover 反色背景。药丸内容为 **费用 · 账户余额 · 当前时段(高峰/低谷/历史)**,与官方统计药丸不重复
  (官方那条给轮次/用时/TPS 与 token 总量/缓存命中率,本插件不再重复这些)。
  参考取值:
  ```css
  /* 官方 packages/client/ui-chat/src/client/chat/StatsPills.module.css(哈希类名已换成 dsh-cost- 前缀) */
  .root{max-width:var(--dsh-chat-content-width);width:100%;padding:4px calc(var(--dsh-composer-side-clearance) + 16px) 0;
        font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));
        justify-content:center;gap:12px;margin:0 auto;display:flex}
  .pill{border-radius:24px;padding:1px 8px;gap:6px;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary);
        font:inherit;font-variant-numeric:tabular-nums;background:0 0;border:none}
  .pill svg{flex:none;width:14px;height:14px}
  button.pill:hover,button.pill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
  .sep{color:var(--dsw-alias-separator-primary);margin:0 6px}
  ```
  图标是自绘的 **¥ 硬币**(官方图标集 `ui-primitives` 里没有钱币/费用类图形,只有 gauge、database、clock、api 等),
  按官方同一绘图规格:16 viewBox、纯描边、`strokeWidth 1.25`、`currentColor`、显示尺寸 14px。
  官方自带的统计药丸继续保留,本插件只补一条费用药丸,不重复统计。
- **模型归属**:按模型拆分调用次数与金额,多模型时自动分列。
- **读图金额**:把会话里读图(视觉输入)的 token 单独记账。图片 token 按 DeepSeek 官方规则估算(进模型前自动缩放,单张上限 384 token,按官方计算器实测口径线性拟合,平均误差 ~5%)。
- **账户余额**:药丸中段显示「余额 ¥X」,进会话先拉一次、之后每 5 分钟自动刷新,来源为官方 `GET /user/balance`。
  **点击药丸立即刷新**:同时重取余额与费用明细(跳过 5 分钟定时与 15 秒节流),点击时**硬币图标转一圈**
  (0.7s 线性、`prefers-reduced-motion: reduce` 下自动关闭;每次点击用 React key 重新挂载 svg,保证动画重播),
  刷新中带 `aria-busy="true"` 与 `title="刷新中…"`,明细卡里「余额」行会标注本次刷新时间(如「(02:45:28 刷新)」)。

## 安装

插件分为宿主半(Node 服务端)与客户端半(浏览器)。本包自带 bundle patch
(`package.json` 的 `dsh.bundle.patch` → `cordis.patch.yml`),**不需要手写 profile 补丁**:

```bash
dsh plugin --profile web add github:david0702/dsh-cost
```

然后重启 dsh 即可(客户端改动刷新页面即生效;宿主改动需要重启进程,宿主插件不会热载宿主代码)。

上一步在做什么:`dsh plugin add` 跑完 pnpm 后会自动核对已安装依赖——凡是声明了
`dsh.bundle.patch` 的包会被加进 profile 的 `dsh.profile.bundles`,成为一层 bundle patch;
本包的 patch 只插入一条插件自有条目:

```yaml
# cordis.patch.yml(本包自带,无需手工复制)
- insert:
    - id: dsh-cost
      name: '@david0702/dsh-cost'
```

### 三个名字必须一致(最容易踩的坑)

DSH 的 client-modules 按**包名**(`node_modules/<pkg>/` 的 `<pkg>`)查找客户端工厂,
而 `lib/client.js` 是自注册脚本 `window.__ModuleLoader__.load({ id: "@david0702/dsh-cost", … })`。
因此下面三者必须同名,本包已自洽为 `@david0702/dsh-cost`:

1. `package.json` 的 `name`
2. 安装目录名(`node_modules/<pkg>/`)
3. `lib/client.js` 里 `__ModuleLoader__.load({ id })`

不一致时会报 `bundle … loaded without registering "@david0702/dsh-cost"`,
并连累整个初始批次(整页 “Failed to load plugins”)。**不要**把本包放进官方
`@deepseek-ai/*` 命名空间,也不要改 `id` 去迁就目录名——第三方插件只应使用自有 scope。

验证是否进组合(浏览器控制台):

```js
__DSH_BOOT__.entries.map((e) => e.id).filter((id) => /cost/i.test(id))
// 期望输出 ["@david0702/dsh-cost"];空数组说明条目没进组合
```

## 开发

```bash
# 把工作区仓库直接链进 profile(pnpm link,改仓库文件即改插件,不产生副本)
dsh plugin --profile web add /path/to/dsh-cost
```

链入后仓库就是 profile 的实体,**不会再出现"profile 副本与仓库漂移"**。

改动生效范围(实测口径):

| 改动 | 生效方式 |
|---|---|
| `lib/client.js` | **保存即热更**:`dsh-client-modules` 监视客户端 bundle,内容变了就重算哈希、换 `rev`,前端重新拉取执行;刷新页面即可见,**不需要重启 `dsh web`** |
| `lib/index.js` | **必须重启 `dsh web`**(宿主插件不会热载宿主代码) |
| `package.json` 的 `dsh` 段(patch / client / 兼容表) | 重启(profile 组合在启动时合成) |
| `README` / `docs` / 版本号 | 只随提交走,不影响运行 |

日常循环:

1. 在仓库里改代码(**不要再往 profile 目录里改**,也不要留 `client.js.bak-*` 之类手工备份)。
2. 自检:`npm test`(槽位时序 + 上架契约),必要时 `node --check lib/index.js`。
3. 看效果:客户端改动刷新页面;宿主改动重启 `dsh web`。
4. 版本与兼容:用户可见行为变了就提 `version`;**只有真的跑过某个官方 DSH 版本的一次性 Profile,才把 `dsh.compatibility.dshReleases` 里那一项改成 `compatible`**,并在 `docs/store-conformance.md` 记录环境与步骤。
5. 提交并 `git push origin master`(DSH STORE 每 8 小时自动复检,不需要在 Issue 里回复)。

不打扰日常实例的做法:日常 `web` profile 从 GitHub 固定提交安装,另建开发 profile 指向工作区

```bash
dsh --profile dev --from-default-profile web   # 首启初始化
dsh plugin --profile dev add /path/to/dsh-cost
dsh --profile dev --port 3099 --no-open        # 与日常端口并存
```

## 配置

无必填配置。API Key 走 DSH 的 credentials(`ctx.credentials.resolve("DEEPSEEK_API_KEY")`),用于拉取余额。

## 兼容性

- 依赖 DSH 的具体版本与约定:
  - 客户端使用 `conversation.composer.dock` 槽位、`props.useProjection("tokenUsage")`、`props.useProjection("sessionStats")`、`props.modelDirectories`。
  - 宿主使用 `ctx.webServer.register`、`ctx.credentials.resolve("DEEPSEEK_API_KEY")`、Node 全局 `fetch`。
- **逐版本兼容声明在 `package.json` 的 `dsh.compatibility.dshReleases`**(对应官方发布逐项 `compatible` / `incompatible` / `unknown`),
  实测口径与证据记录见 [`docs/store-conformance.md`](docs/store-conformance.md):**只有真实跑过一次性 Profile 安装/启动/卸载的版本才标 `compatible`**(当前为 `0.1.5-rc.1`),未验证的一律 `unknown`。
  宽泛范围(`dsh` 字段)只作参考,不构成"可安装证据"。
- **槽位注册必须走 `ctx.slots.inject(slot, cb)`**(DSH 0.1.5+):
  `conversation.composer.dock` 由 `client-ui-conversation` 的 composer-bar 条目在其 children 表里声明,
  只在该条目挂载期间存在;直接调 `ctx.slots.register` 会抛
  `slot "conversation.composer.dock" is not declared (a parent entry's children table must declare it)`,
  且该异常会顶着宿主 loader 条目名上报(表现为整页 “Failed to load plugins” 的报错块)。
  `slots.inject` 会把注册推迟到声明提交之后,声明消失时自动注销、恢复后重注册。
  离线自检:`npm test`(`scripts/slot-inject-check.mjs`,模拟“先 apply、后声明”的时序)。
- **`lib/client.js` 里 `__ModuleLoader__.load({ id })` 必须与 loader 条目解析出的模块 id 一致**
  (即 profile 里 `node_modules/<pkg>/` 的目录名)。对不上时 client-modules 会报
  `bundle ... loaded without registering "<pkg>"`,且会连累整个初始批次,表现为整页 “Failed to load plugins”。
  本包三处名字统一为 `@david0702/dsh-cost`(见上节),不要为了让目录名迁就官方 scope 而改 `id`。
- 定价与高峰时段常量写死在 `lib/index.js` 的 `RATES` / `PEAK_EPOCH_UTC`,按官方发布更新。
- 实际 token 数以模型接口返回为准;`imageTokensOf` 与卡片金额为 DeepSeek 估算口径(官方说明:估算值,以接口返回为准)。

## License

MIT

Install

dsh plugin --profile web add github:david0702/dsh-cost

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source