DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

CHIP-PHILO-GH /

CHIP-PHILO-GH/dsh-turn-cost

Verified

DSH 插件:在本轮统计与回合尾部显示花费的人民币金额,只读既有槽位与用量事件,不改宿主。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a70ccbb2

License: MIT CI

dsh-turn-cost —— DSH 轮次花费(人民币)

只想要“照着做一遍”的封装版本(含安装、配置与排错表),见同名技能包 dsh-turn-cost;本仓库是代码本体。

在 DSH 输入框下方、以及每一轮回复的尾部,显示这一轮和整个对话花了多少人民币。

这是什么

一个 DSH 插件,两端一模块:

位置 文件 职责
宿主端 lib/index.js 注册一个只读的会话投影 turnCost,把每次模型调用的 token 桶按轮次折叠成金额
计价 lib/pricing.js 读价格表 / 汇率 / 峰谷规则,把 token 桶换算成人民币
浏览器端 lib/client.js 从投影取值,在 DSH 既有界面上就地加两个元素
装载行 cordis.patch.yml 往 profile 补丁层插一条 loader 条目
离线测试 test.mjs 只覆盖 lib/pricing.js,见「测试」

宿主端只做加法:不修改 DSH 任何源码、样式或既有插件。它注册的是会话投影(对持久会话日志的纯 fold), 不介入模型调用链。

解决什么问题

DSH 界面上本来就有「1 轮 71 步·257 tok/s」「10.7M tok·缓存命中 99%」这类统计,但没有「这一轮花了多少钱」。 本插件把 usage 里的 token 桶按 dsh-cost-meter 账本里的价格换算成人民币(缺省符号 ¥、4 位小数),补上这一格:

  • 输入框下方 —— 看整个对话累计花了多少;
  • 每轮回复尾部 —— 看这一轮花了多少。

显示在哪、长什么样

两个落点都是「就地加一个元素」,不新开行、不新开面板。

1) 输入框下方的统计药丸

在 DSH 自己那一排统计药丸里追加第三个,文本形如:

本轮 ¥0.42 · 本对话 ¥3.17

本对话累计金额为 0、或投影还没有数据时,这一行什么都不加。 悬停提示按行给分项(金额、四个 token 桶、模型调用次数):

本对话 · 金额 ¥3.17 · 未缓存输入 2.1M tok · 缓存读取 10.4M tok · 缓存写入 0 tok · 输出 186K tok · 模型调用 63 次
本轮 · 金额 ¥0.42 · 未缓存输入 320K tok · 缓存读取 1.8M tok · 缓存写入 0 tok · 输出 21.3K tok · 模型调用 9 次

2) 每个回合尾部的徽章

在每个回合的动作条(与复制 / 点赞 / 分叉按钮、「用量 X tok」同排)里加一枚小徽章:

本轮 ¥0.42

它显示的是这条助手消息所属轮次的金额——宿主端维护了「助手消息 id → 轮次号」的映射,徽章靠它定位。 映射找不到、或那一轮金额为 0 时,不渲染任何东西。

样式

自带样式写在插件自己的 <style data-plugin-css> 标签里,类名统一 dtc- 前缀, 颜色一律取全局主题变量(--dsw-alias-label-tertiary 等),不引用、也不修改 DSH 的任何类名与样式表。 药丸复刻邻居药丸的规格(继承行内字号与行高、1px 8px 内边距、24px 圆角、tabular-nums), 所以它看起来和旁边两个是同一排的。

数据从哪来

宿主端:turnCost 会话投影

lib/index.js 通过 ctx.inject(['sessionProjections'], …) 注册一个 key 为 turnCost 的会话投影, 从会话事件流里读两类用量样本:

事件 取什么
assistant/chunk,且 chunk.type === 'usage' chunk.usage,加上 data.turn / data.step
assistant/message data.usage,加上 data.turn / data.step

usage 的四个桶按字段名映射:inputTokens → 未缓存输入、outputTokens → 输出、 cacheReadTokens → 缓存读取、cacheWriteTokens → 缓存写入;每一项都先夹到 ≥ 0(负数与非数字当 0)。

另外两件事:

  • request/header 事件里的 data.header.config.model 决定当前模型 id;取不到时用 default;
  • assistant/message 的 data.message.id(或 data.messageId)用来维护「助手消息 id → 轮次号」的映射。

同一轮不会重复计数

流式过程中会先到一个不完整的 usage 样本,回复结束时会再来一个最终样本。投影按 轮次:步 折叠:

  • 同一个 轮次:步 的新样本与上一个完全一致 → 直接跳过 (这顺带避免了切模型后重复上报同一样本把「模型调用次数」多算一次);
  • 不一致 → 先把旧样本减掉,再把新样本加上;
  • 只有全新的 轮次:步 才把「模型调用次数」+1。

客户端:只读投影

lib/client.js 用 props.useProjection('turnCost') 取值;取不到(宿主端没加载、或投影尚未建立)时什么都不显示。 宿主端对客户端只暴露显示需要的字段:symbol / decimals / totals / turns / latestTurn / messageTurn。

安装

前提:Node ≥ 18,本机已装 DSH。

运行时的 zod 由 DSH 宿主提供,本包在 package.json 里以 peerDependencies 声明它("zod": "*"), 不把宿主运行时复制进自身依赖树。因而:在 DSH 里装载不需要额外安装;单独 npm install 本包时, npm 会按 peer 规则自行解析 zod。

git clone https://github.com/CHIP-PHILO-GH/dsh-turn-cost
cd dsh-turn-cost

把这个目录装成名为 dsh-turn-cost 的 DSH 插件——放进 profile 的插件目录,例如 $env:DSH_HOME\profiles\node_modules\dsh-turn-cost(DSH_HOME 未设置时是 ~/.dsh)。

装载行由本包自带的 cordis.patch.yml 提供(package.json 的 dsh.bundle.patch 指向它), 内容是往 profile 补丁层插一条 loader 条目:

- insert:
    - id: turn-cost
      name: 'dsh-turn-cost'

装好后重启 dsh web 生效(宿主端与浏览器端 bundle 都只在启动时加载,本插件不做热重载)。 重启后宿主端会打印一行自检日志,里面的三个值就是本次生效的符号 / 小数位 / 汇率:

[dsh-turn-cost] 已加载,计价:¥ 小数 4 位,汇率 7.2

卸载

  1. 从 profile 的 cordis.patch.yml 里删掉 id: turn-cost 那条 loader 条目;
  2. 删掉插件目录 …\profiles\node_modules\dsh-turn-cost;
  3. 重启 dsh web。

插件自身不写任何文件,也没有自己的配置文件,所以没有残留状态要清理(页面上那两个元素随页面刷新消失)。

配置项

本插件没有自己的配置文件。 价格口径直接沿用 dsh-cost-meter 的账本配置:

<DSH_HOME>\storages\cost-meter\ledger.json   →   顶层 "config" 字段

<DSH_HOME> 取环境变量 DSH_HOME,未设置时取 ~/.dsh。 这个文件不存在、读不了或格式不对时,插件不会报错,而是整份退回内置兜底配置(见「计价依据」)。

config 里被读取的字段一共这些,其余字段一律忽略:

键 类型 缺省 说明
symbol string ¥ 金额前缀。空串或非字符串时用 ¥
decimals number 4 显示小数位。先向下取整,再夹到 0–10
exchangeRate number 7.2 美元 → 人民币汇率。非正数或非数字时用 7.2
peakEnabled boolean false 峰谷计价开关。必须严格等于 true 才启用(字符串 "true" 不算)
peakEffectiveAt string 无 峰谷档生效时刻,交给 Date.parse 解析。该时刻之前的调用一律按基础价
peakWindows array [] 峰时段窗口,单位是 UTC 小时,见下
prices.models object {} 模型 id → 单价记录
prices.default object 兜底单价 模型没登记时用它

单价记录(prices.models.<模型 id> 与 prices.default)的字段:

键 说明
cacheHit 缓存命中价(缓存读取与缓存写入都用它)
cacheMiss 未命中输入价
output 输出价
offPeak 可选。非峰时段档位,字段同上;缺失时退回基础价
peak 可选。峰时段档位,字段同上;缺失时退回基础价

三条归一化规则:单位一律是美元 / 1M tokens;记录里缺哪个字段,就用兜底单价里的同名字段补; offPeak / peak 里缺的字段再用该记录自身的基础价补。

peakWindows 的每一项是 { start, end },单位是 UTC 小时(0–23):

  • start < end:命中区间 [start, end);
  • start > end:跨零点环绕,命中 [start, 24) ∪ [0, end);
  • 某项的 start / end 不是有限数字 → 该项被忽略。

peakEnabled 为 true 时还有一条容易踩的规则:peakEffectiveAt 必须是能解析成时间的字符串。

  • 有效:该时刻之前的调用一律按基础价;之后落在窗口内用 peak 档,其余用 offPeak 档;
  • 无效或缺失:落在窗口内仍然用 peak 档,但窗口外一律用基础价——offPeak 档不会被用到 (代码只在「调用时刻 ≥ 生效时刻」时才取 offPeak)。

判定用的是调用发生的时刻(事件自带时间戳,缺失时退回当前时刻),不是「现在」。

⚠️ 配置在插件加载时读一次。改完账本要重启 dsh web 才生效。

计价依据

公式

美元成本 = (未缓存输入 × cacheMiss + 输出 × output + (缓存读取 + 缓存写入) × cacheHit) / 1,000,000
人民币   = 美元成本 × exchangeRate

四个 token 桶都先夹到 ≥ 0。缓存写入按命中价(cacheHit)计费,口径与 dsh-cost-meter 一致。 汇率不是有限正数时按 1 处理。

兜底单价(写死在代码里的那份)

lib/pricing.js 的 FALLBACK_ENTRY,单位 美元 / 1M tokens, 代码注释标明对应 DeepSeek 官方 deepseek-flash 档:

项 单价(美元 / 1M tokens)
缓存命中(cacheHit,含缓存读取与缓存写入) 0.0028
未命中输入(cacheMiss) 0.14
输出(output) 0.28

兜底配置的其余部分:符号 ¥、小数位 4、汇率 7.2、峰谷开关关闭、prices.models 为空。

按兜底表算几个数(汇率 7.2):

用量 美元 人民币
1M 未命中输入 0.14 ¥1.008
1M 输出 0.28 ¥2.016
1M 缓存读取 0.0028 ¥0.02016
1M 缓存写入 0.0028 ¥0.02016
1M 未命中输入 + 1M 输出 + 1M 缓存读取 + 1M 缓存写入 0.4256 ¥3.06432

有账本配置时

只要 ledger.json 读得到,用的就是账本里的价格表、汇率、小数位与峰谷规则; 内置那份兜底表只在读不到的时候整份顶上(模型级单价缺失时退回 prices.default,default 也缺失时退回兜底单价)。

能力边界与已知限制

它给的是估算,不是账单。 以下是代码层面能确定的口径与边界:

  1. 只统计 usage 的四个 token 桶。会话事件流里 usage 之外的任何计费项都不计入。
  2. 价格依赖账本。账本读不到时按内置兜底表(DeepSeek 官方 deepseek-flash 档 + 汇率 7.2)算。 你的模型或渠道不在账本里、或账本价格与你的实际合同价不同时,显示值会与实际扣费不一致。
  3. 缓存写入按命中价计。这是与 dsh-cost-meter 对齐的口径;渠道对缓存写入另有定价时,这里不会体现。
  4. 峰谷档按 UTC 小时判断,且 peakEffectiveAt 之前的调用一律按基础价; peakEnabled 不是布尔 true(例如配置里写成字符串)时峰谷规则完全不生效; peakEffectiveAt 无效或缺失时,窗口外的调用不会走 offPeak 档,而是按基础价。
  5. 配置只读一次(插件加载时),改账本要重启 dsh web。
  6. 金额是四舍五入后的显示值:按 decimals 位 toFixed 后去掉末尾的 0; 金额小于 10^-decimals 时自动多给两位小数(最多 12 位),所以极小金额不会显示成 ¥0。
  7. 「本轮」= 轮次号最大的一轮,不是「正在跑的那一轮」。
  8. 只有一轮时,两段文案会重复显示同一个金额:输入框下方的文案本意是「只有一轮时不重复两遍」, 但那条合并条件比较的是对象引用,而 turns 里的条目和 totals 不是同一个对象, 所以一轮时仍会显示「本轮 ¥x · 本对话 ¥x」。这一条是从代码读出来的,没有在浏览器里逐帧实测。
  9. 输入框下方的药丸靠 DOM 落位:它选的是带 [data-composer-stats] 标记属性的那个容器。 宿主哪天改掉这个标记,药丸就不再出现(回合尾部的徽章走的是插件槽位,不受影响)。 插件对 DOM 变化挂了 MutationObserver:那一行被会话切换重建、或宿主往末尾追加自己的节点时, 会自动把自己插回并挪到最后一个位置,同时认领已存在的同类节点以避免同一行出现两个。
  10. 宿主端没起来就什么都不显示:客户端取不到 turnCost 投影时静默不渲染(不报错、不占位)。

测试

node test.mjs

test.mjs 零依赖(只用 Node 内置模块),也不需要 DSH 在运行,更不需要装 react / zod。 它覆盖 lib/pricing.js——本插件里唯一能在离线环境判定的部分,因为它是个纯函数模块, 只依赖 node:fs / node:path / node:os。测的内容:

  • 兜底单价、符号、小数位、汇率、峰谷开关;
  • 从临时 ledger.json 解析配置,含 decimals 夹取、exchangeRate 兜底、peakWindows 跨零点环绕;
  • 模型单价查找与回退(模型 → prices.default → 兜底单价);
  • 档位选择(峰谷关闭 / 生效前 / 峰时段 / 非峰时段 / 档位缺失);
  • 金额公式(四个桶、负值夹 0、汇率非法按 1);
  • 发布字段:包名、无 private、engines、repository、files、版本号与 CHANGELOG 一致;
  • 改名的三处一致性:package.json 的 name == cordis.patch.yml 里的 name == lib/client.js 里 window.__ModuleLoader__.load({ id }) 的 id(客户端模块按这个 id 注册,对不上会加载失败);
  • 文档一致性:每个被读取的配置键都必须在 README.md 与 README.en.md 里出现, 且 README 里的兜底单价与汇率与代码一致;
  • 仓库里所有文本文件都不含本机路径片段。

CI 跑的就是这一条命令(见 .github/workflows/ci.yml),任何 Node ≥ 18 的机器上都能跑通。

没有覆盖、也没放进 CI 的部分:

部分 为什么
lib/index.js(宿主端投影) import { z } from 'zod',且投影要跑在 DSH 的 sessionProjections 上下文里
lib/client.js(浏览器端) 单文件 bundle,靠 window.__ModuleLoader__ 加载,要真实浏览器 + DSH 页面
端到端(真的显示出金额) 需要 DSH 正在运行,并且有一次真实模型调用

这三部分只能手动核验:重启 dsh web 后看那行自检日志,再在一次真实回复后看输入框下方有没有多出 「本轮 ¥… · 本对话 ¥…」药丸、以及该轮尾巴上有没有徽章。本仓库没有对它们做自动化验证。

许可

MIT。

离线验证

陌生人 clone 后可直接运行:

node test.mjs

该命令只使用 Node 内置模块,实际 import lib/pricing.js,覆盖兜底配置、计价公式、模型/峰谷单价选择、账本解析、边界处理和发布字段一致性。输出会给出“离线可测 N 项 / 需真实环境 M 项”,并明确打印每个 SKIP;React 渲染、DSH runtime 装配、真实用量事件和浏览器页面不在离线覆盖范围内。

—/ 5

No ratings yet

Verified DSH bundle

Commit a70ccbb2e251

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout