← Back to home@david0702

dsh-cost

DSH (DeepSeek Harness) 对话底部费用显示插件:按每笔请求时间+模型分批计费,分时段明细,模型归属,读图金额,余额。

Stars
0
Language
JavaScript
Created
Aug 26, 2026
Updated
Sep 30, 2026
GitHub repo

Introduction

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 总量/缓存命中率,本插件不再重复这些)。 同处一行、不换行、不额外占行:conversation.composer.dock 本身是一个 display:flex(不 wrap)的行, 官方统计药丸与上下文计量都在这行里;本插件的根节点用收缩自适应的 inline-flex 挤进同一行, 而不是像官方 StatsPills.root 那样 width:100% 独占整行。 参考取值:
    /* 官方 packages/client/ui-chat/src/client/chat/StatsPills.module.css(哈希类名已换成 dsh-cost- 前缀)。
       注意:官方 .root 是 width:100% + justify-content:center 的整行 wrapper;本插件要并入同一行,
       故根节点改为收缩自适应,其余药丸取值与官方一致。 */
    .root{display:inline-flex;align-items:center;min-width:0;max-width:100%;box-sizing:border-box;
          font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));
          gap:12px}
    .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 补丁:

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

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

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

# 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。

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

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

开发

# 把工作区仓库直接链进 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 指向工作区

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")),用于拉取余额。

权限与已知风险

面实际情况
网络只访问 https://api.deepseek.com(GET /user/balance),由宿主半发起;API Key 不出服务端、不下发浏览器
凭据读取 DEEPSEEK_API_KEY(经 ctx.credentials.resolve),仅用于上面那次余额请求
文件 / 命令不读写文件、不派生子进程;会话数据经宿主服务(sessions / sessionQuery / sessionPersistence)只读取得
客户端只向同源 /api/dsh-cost/read、/api/dsh-cost/balance 发 fetch,不加载任何第三方脚本

已知风险:

  • 两个 /api/dsh-cost/* 路由不经过官方 /api 浏览器信任栅栏:官方栅栏是 kind: 'prefix' 的 /api 路由, 本插件以 kind: 'exact' 注册同前缀下的路径,而精确路由先匹配;官方也没有把该信任判定暴露为可注入服务(第三方插件无法复用)。 实测(0.2.0-rc.2,均不带凭据):官方 /api/sessions、/api/workspaces → 401 unauthorized; 本插件 /api/dsh-cost/read → 404 {"ok":false,"error":"session not found"}、/api/dsh-cost/balance → 200(处理器已被执行)。 因此在配置了 DEEPSEEK_API_KEY 的机器上,同机其它进程可无凭据读取当前会话费用与账户余额。 web 服务默认仅监听本机;不要把 dsh web 绑到不可信网络。细节与建议见 docs/store-conformance.md §五。
  • 金额是按官方公开单价(含读图 token 的线性拟合)的估算,以模型接口返回与实际账单为准。

兼容性

  • 依赖 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:只有真实跑过一次性 Profile 安装/启动/卸载的版本才标 compatible(当前为 0.1.5-rc.1、0.2.0-rc.2),未验证的一律 unknown。 宽泛范围(dsh 字段)只作参考,不构成"可安装证据"。
  • 文案走自有 locale 命名空间:用户可见文案全部由 dsh-cost 命名空间承载, apply 里 ctx.locale.register("dsh-cost", "zh"|"en", dict) 各注册一份,槽位注册用 locale: "dsh-cost" 取得 props.t; exports.inject 相应声明 ["slots", "locale"]。时段文案按 Host 返回的 mode 码(peak/offpeak/hist)本地化, 不直接展示 Host 的中文 label。离线门禁:npm run check:locale(注释与中文词典之外不得有中文字面量、中英键集必须一致)。
  • 槽位注册必须走 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、后声明”的时序并校验 locale 命名空间, scripts/locale-check.mjs 校验文案归属与中英键集平衡)。
  • 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