← 返回列表
⚠ 装前注意
为 DeepSeek Harness 提供透明、可审计的 token 记账。
基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/9/13 · 已提供中文文档
为 DeepSeek Harness 提供透明、可审计的 token 核算——可安全重启的账本,外加一个 CLI,可从原始日志重新计算并对比结果。
综合分
29.7
GitHub 分
29.7
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add chenmiao8563/dsh-token-ledger未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
信任档位:已验证本站已于 1 天前真实安装成功
- 是什么
- dsh 原生插件 · market
- 装得上吗
- 本站已真实安装成功(非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 12 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/23(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@chenmiao8563/dsh-token-ledger(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22.15.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
未发布到 npm registry,仅可从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/23 21:27:03
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-settings-general@deepseek-ai/dsh-commands@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-session-persistence用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成dsh-token-ledger
为 DeepSeek Harness 提供透明、可审计的 token 记账。
CI
npm
license
这是什么
这是一本账本,不是一块仪表盘。它把 DSH 的持久会话日志折叠成 token 用量,保证重启不丢,
而且——这是关键——允许你从原始日志重算同一批数字并与之对账。
$ dsh-token-ledger audit
scanned 139 session log(s), 344203 events, 29 fork(s), 0 unreadable
stored 6901 calls 1205685663 tokens
recomputed 6901 calls 1205685663 tokens
audit: match — the stored ledger equals a fresh fold of the raw logs
只想看漂亮的图表,市面上有十几个同类插件。这个插件是给你需要为这个数字辩护的时候用的。
从 0.5 起,设置页有三个页签:概览(账本本身,含每档区间的预计花费)、费率(各厂商
最新模型的价格表与实时美元汇率)、账单(把同一批用量折算成钱——按工作区 / 会话 / 模型 /
供应商四个分组自上而下展开,可整页导出 CSV 或 JSON)。账单页会写明价格来源、汇率,
以及每一个没能套上价格的模型,所以这个数字是可以核对的;概览页只数 Token,
费用是拿账单那套算法算出来的估算值。详见设置页。
为什么别的插件装不上时它能装上
| 特性 | 为什么重要 |
| --- | --- |
| 没有任何东西会被装给你 | 无运行时依赖;声明的 peer 全都是可选宿主包——DSH 内部包的版本漂移不可能把第二份 harness 拖进你的 profile。 |
| 零安装脚本 | 直接用 git URL dsh plugin add 即可——pnpm 没有构建要拦,你也不必去 allowBuilds 里加白名单。 |
| 只 import node: | 宿主端可以从任意 profile(web / desktop / headless / TUI)加载,不需要解析任何包。 |
| 不碰模型可见面 | 它不注册任何提示词段、消息或工具,因此不会改变请求前缀,也不会损害 KV cache 复用。 |
| 故障降级 | 每个钩子都有保护。账本出问题只记一条警告,绝不让会话失败。 |
安装
从 npm
dsh plugin --profile web add @chenmiao8563/dsh-token-ledger
从 git URL(不涉及任何构建步骤)
dsh plugin --profile web add github:chenmiao8563/dsh-token-ledger
从本地仓库
dsh plugin --profile web add /absolute/path/to/dsh-token-ledger
npm 包名带 scope,是因为 npm 会把分隔符归一化后比较,无 scope 的
dsh-token-ledger 被判为与已有包过于相似而拒绝发布。CLI 命令名仍然是
dsh-token-ledger。
重启 DSH,然后确认那一行进去了:
dsh --profile web --dump-config | grep token-ledger
在对话里用 /tokens:
/tokens
/tokens export # 把 CSV 与 JSON 写到 /token-ledger/exports/
/tokens json # 原始快照
/tokens path # 账本文件位置
账本写在 /token-ledger/ledger.json。
需要的 DSH 版本
DSH ^0.1.2-rc.1 —— 0.1.2-rc.1 是本插件开发与实测所针对的版本,也就是这个区间的下界。
^ 把上界放在 0.2.0:这里用到的接口面是在 0.1.2 这一线上看过的,不是对未来版本测过的。
这个要求按生态里真正会被读取的方式声明——写成对两半各自绑定到的五个宿主包的
peerDependencies:
| Peer | 为什么要写它 |
| --- | --- |
| @deepseek-ai/dsh-session-persistence | 它提供 sessionPersistence,账本的用量数据来自这里。 |
| @deepseek-ai/dsh-commands | 它提供 commands,/tokens 靠它注册。 |
| @deepseek-ai/dsh-host-webserver | 它提供 webServer,设置页那三个路由靠它提供。 |
| @deepseek-ai/dsh-client-locale | 浏览器半边注入它来拿翻译后的文案。 |
| @deepseek-ai/dsh-client-ui-settings-general | 浏览器半边注入它,把分区加进设置侧边栏。 |
五个都是可选(optional),这是刻意的而不是含糊其辞:DSH 不会把宿主包提升进 profile 的
node_modules,所以写成必需 peer 只会在每次安装时报告一个"未满足"、却什么也不说明。可选也正是
插件自身行为的如实描述——没有 web 服务器的 profile 少掉设置页,/tokens 与 CLI 照常可用;
headless 或 TUI profile 则根本没有那两个客户端包。
这个声明之所以有用,是因为 DSH 会拿实际在跑的那套安装去解析它:profile 检查器先看插件自己的
node_modules,再看 profile 目录树,最后回落到 DSH 安装目录本身
(node_modules/dshmarket/lib/check.js),把解析到的版本与上面的区间比对——于是不受支持的
harness 会被报出来,而不是悄悄挂载。早先的版本声明的是 dsh.compatibility.dsh,而 DSH 里
没有任何代码读它;1.0 把它删掉,而不是留一个看起来像承诺、实际不是的字段。
这些声明对消费者零成本:可选 peer 不会被安装,包本身依旧没有运行时依赖、没有安装脚本。
计数规则
只有知道它到底在数什么,这些数字才有用。
| 规则 | 行为 |
| --- | --- |
| 只认成功锚点 | 用量取自 assistant/message(已完成的步)与 compaction/summary(一次压缩调用)。失败或被取消的尝试不会追加这两种事件,因此永远不计入。 |
| 流式样本是替换,不是相加 | 同一步先出 usage chunk、后出最终消息时,最终值替换早期样本——无论哪种情况都算一次调用。 |
| totalTokens 是推导出来的 | 它是四个桶之和,绝不采信提供方自己的总量字段。在 15,778 份真实用量报告上两者完全一致,而推导能让桶与总量在构造上永远自洽。 |
| 推理 token 是子集 | 单独报告,绝不相加进总量,因为它本来就在 outputTokens 里面。 |
| 按本地日历日 | 是你所在时区的日,不是 UTC 日。 |
| fork 切、resume 不切 | 见下。 |
fork 与 resume 的区别
存储的日志可能以一段"已经记录过的历史"开头。有两种完全不同的情况会产生这种前缀,
把它们搞混是用量插件静默算错的最常见原因:
- fork(有 parentSession):前缀是父会话的历史,已经在父会话自己的日志里计过。
在这里再计一次就是重复计入——必须切掉。
- resume(没有父会话):前缀是这个会话自己更早的历史,只存了一次。
切掉就是漏计——不能切。
这不是猜的。对照真实日志:所有父日志仍在磁盘上的 fork 会话,其边界标记之前的用量指纹
都包含在父会话里;而带同样标记但没有父会话的日志,其前缀在文件后半段从未重复出现。
在一个真实的 139 会话 home 上,这个区别意味着 7,992 万 token 的重复计入——那是
"把每个日志都完整计一遍"的天真做法会报出来的数字。
命令行
不需要 DSH 在运行——它直接读原始日志。
dsh-token-ledger [summary] [选项] 打印已存账本(默认)
dsh-token-ledger audit [选项] 从原始日志重算并对比
dsh-token-ledger rebuild [选项] 从原始日志重算
dsh-token-ledger export [选项] 导出 CSV 与 JSON
--home DSH home(默认 $DSH_HOME,其次 ~/.dsh)
--ledger 要读写的账本文件
--out 导出目录
--days 摘要显示天数(默认 7)
--models 摘要显示模型数(默认 5)
--write 配合 rebuild:覆盖已存账本
--json 机器可读输出
--quiet 抑制人类可读摘要,只保留退出码
退出码:0 成功或审计通过,1 审计发现真实差异,2 用法错误或输入不可读。
因此它可以挂进定时任务。
审计能区分两类差异
运行中的宿主是按防抖写入账本的,所以活跃会话比文件"新一点"是常态。
把这种情况报成数据损坏,审计就废了。账本自己的 updatedAt 可以裁决:
- 有差异的会话,其最新事件比账本更新 → 只是还在跑 → 通过,并报出尚未落盘的量;
- 有差异的会话,其最新事件早于账本,或者某条日/模型行与它本该汇总的折叠结果矛盾
→ 不通过,退出码 1。
audit: match — 1 session(s) advanced after the ledger was written
(1 calls, 5100 tokens not yet flushed)
设置页
浏览器端会在设置侧边栏注册一个 用量账本 分区,顶部有 概览 / 费率 / 账单 三个按钮。
概览
- 区间总计:本月 / 本年 / 近 7 天 / 全部四档可切换,每档显示 Token 总计、缓存命中率、
调用次数、预计花费,以及背后的四桶明细。「全部」就是账本里所有天数,起止跟着第一条记录走。
缓存命中率定义为
缓存读入 / (缓存读入 + 未命中输入)——即输入中被缓存吸收的比例,因此完全不缓存的
路由读数是 0%,而不是空白。
- 预计花费用的就是账单那套算法:由宿主针对概览的每个区间各算一次,因此「本月」这一个
数字和账单页本月那一行是同一个数,而不是两套算法今天恰好对得上。以人民币显示、保留两位
小数,并注明换算用的汇率。宿主还没拿到价格时显示的是短横线并说明原因,而不是一个理直气壮的
¥0.00;账单算不出价的那部分 Token 会写在它缺席的那个数字下面。
- 今日实时:今天的 Token、命中率、调用次数与预计花费,每分钟刷新一次。
- 用量热力图:年 / 月 / 一周三档可切换;年和月是热力图,切到一周改为每天一行
的横向条形图。
热力档位相对窗口内最忙的一天取平方根,避免某一天特别大把其余全部压成最淡档。
月视图右侧另给本月小结:最忙的一天、最轻松的一天(仅工作日)与工作到最晚的一天
(仅工作日,按当天最后一次调用的时刻比较)。
- 按模型:各模型的用量、各自命中率,以及一条展示用量构成的堆叠条,下方图例只列出真正画出来的
分段。某个桶在整个 payload 里都是 0(比如厂商不公布缓存写价格时的缓存写入),它既不会出现在条里,
也不会出现在图例里——图例里有一个条形图中永远找不到的颜色,那不是信息而是谜题。
费率
- 美元汇率单独一个框,保留四位小数,并标注来源与获取时间。这一页是价目表:
它只报价格、并按汇率换算显示;把价格乘上用量做成费用的是账单页,页面上也写明了这一点。
- 各厂商最新模型:每个厂商只列最新的 2~3 个,给出输入 / 输出 / 缓存读 / 缓存写
每百万 Token 的价格。价格是各家自己公布的价目,取自一份按厂商整理的价格清单,并按
上方汇率换算成人民币显示;每个换算后的单元格悬停时仍能看到原始的美元报价。没取到汇率时
表格回退成美元并在列头标明单位,而不是硬凑一个自己都站不住的数字。厂商没有公布的价格显示为
短横线(「没有标价」和「免费」是两回事);公布为 0 的会额外标注「不一定是免费」,因为有些
平台按 GPU 小时计费、根本不按 Token 标价。
- 价格来自厂商自己公布的地方:这些厂商没有任何一家提供价格 API——它们的模型列表接口
只返回模型 id、不含价格,价格只存在于官网 pricing 页面。能直接读的就去读:DeepSeek、
Z.ai、腾讯三家的价格是当场从他们自己的定价页解析出来的,分组前面带「官方」标记;其余来自一份
按厂商整理的公开数据集(models.dev)。配置 rates.source: openrouter
可把整张表切成该网关自己的报价——覆盖模型更多,但不是各家官方价目。页面会写明哪家是哪种来源。
- DeepSeek 区分高峰与空闲时段:它家页面按人民币报价,分缓存命中输入 / 缓存未命中输入 / 输出
三条线,每条都有高峰与空闲两列——空闲时段价格为高峰的一半,高峰时段是北京时间周一至周五
9:00-12:00、14:00-18:00,其余为空闲。因此每个时段各占一行并带标签,时段说明直接引用厂商原文。
用人民币标价的厂商就按人民币显示,不做换算。
- 只列这 12 家,并用官方标识:OpenAI、Anthropic、Google、DeepSeek、Qwen、xAI、Z.ai、Kimi、
MiniMax、腾讯、小米、字节跳动。图形取自各家官网或 Simple Icons 的官方标识,内联进包里、页面
加载时不联网;它们是各家的商标,仅用于标识价格属于谁。名单外的厂商若数据源里有也会照常出现,
rates.vendors: 0 则列出全部厂商。
- 手动填写:任何一项价格、以及汇率本身,都可以直接改写——按表格当前显示的币种填写。
手动值优先级最高,不会被后续刷新覆盖,确实改动了数值的行在表格里带「手动」标记,也可以一键
「恢复自动」或「清除」;若填入的值与自动获取的相同,则不带标记——此时刷新本来就不会改变这一行。
另有一行自由录入,用于自动获取没覆盖到的模型。
- 随时刷新,逐个确认:价格卡片上有刷新按钮,立即执行一次与定时任务相同的抓取,不必再等半小时;
某个源没连上时页面会说明这次刷新失败,而不是报「已保存」。当你手填过价的模型官方价变了,页面会
单独列出该模型、并排显示两个价格和一个问题——用官方价,还是保留你的。选「用官方价」会删掉这个
手填值,把该字段交还给数据源,而不是把它冻结在今天的价格上;选「保留我的」则记录下当时看到的
官方价,同样的分歧不会再问,直到官价真的再次变动。
- 离线是一种状态,不是错误:宿主每 30 分钟刷新一次价格与汇率。刷新失败不会清空
已有结果,而是把这次尝试标记为失败,因此被防火墙挡住时看到的是「上次已知价格 +
可见的陈旧时间」,而不是一片空白。从未联网的宿主会明确说明并引导到手动录入;
配置 rates: false 的宿主完全不发请求,页面就是一份你自己维护的价目表。
账单
同一批用量,按账单的读法分组。四个分组直接自上而下展开——工作区、会话、模型、供应商——
而不是藏在切换按钮后面,因为账单要回答的通常是「比较」类问题;每个分组各有自己的时间段,
所以「本月的按工作区」可以和「今日的按模型」同时摆在页面上。
- 每行显示:分组名、实际花费、缓存命中输入、未命中输入、输出、缓存命中率、
调用次数。两个输入列分开列,是因为凡是给它们定价的厂商,两者价格都不一样。缓存写入
放在每张表下面一行说明,而不是做成一个几乎处处是短横线的列。
- 名字是有意义的:工作区一行就是账本记录的 cwd;会话一行显示
工作区/会话名,用的正是 DSH 自己给这个会话起的名字(DSH 没起过名的回退成会话 id,
完整 id 与路径放在悬停提示里);模型一行显示 提供商/模型——就是账本记录的那条路由,
与概览页「按模型」列出的名字完全一致——具体按哪份价目计价放在悬停提示里。
- 供应商一行是「你接入的提供商」,不是一份价目表:bos、qwen-plan、
deepseek-official、zai 是接入的端点,deepseek、qwen、z-ai 是这些 Token 按谁家的
价目计价。所以这一行显示的是提供商——用设置页给它的显示名(BOS-API 而不是 bos,
这个名字是从 harness 的 settings 文件里读出来的,绝不写入),下面再写明计价来源
(计价来源 deepseek)。因此同一个模型经由两个端点调用,就是两行——这正是供应商账单要回答的问题。
- 每个分组五个时间段:本月 / 本年 / 7 天 / 今日 / 全部,与概览页同一套定义。
- 会话列表在页面上汇总,导出的文件不汇总:只问了一句就归档的会话不值得单独占一行,而一台机器会攒下几百个,
所以花费很少或几乎没用过的会话——低于 ¥1,或调用少于 10 次——在页面上合并成一行
(其余 N 个低频会话,带「汇总」标记,各列是它们的合计)。总额一分不动:各组行之和仍等于分组合计。
表格下方会写明汇总了多少个会话、规则是什么,并说明导出永远是完整明细——format=csv 里每个会话都在。
两种情况永不折叠:算不出价的会话,以及整张表每一行都符合条件时(那种情况下列表本来就短,折叠反而把列表藏起来)。
bill.smallSessionCost、bill.smallSessionCalls、bill.foldSmallSessions: false 可改规则或关掉。
只有「会话」这一维度会折叠,工作区 / 模型 / 供应商本来就只有几行。
- 右上角一个导出,导出全部:CSV 或 JSON,包含四个分组 × 五个时间段的全部信息,
每行带币种,每个分组各自一行 TOTAL,而且刻意不给跨时间段的总计——这些时间段互相重叠,
把今日加进本周再加进本月会把同一批 Token 数四遍。CSV 文件开头带 UTF-8 BOM:
没有这三个字节,中文版 Excel 会按系统代码页打开它,会话名「编写统计」会显示成
「缂栧啓缁熻」;JSON 导出不带 BOM,因为 JSON 解析器会直接拒绝开头有 BOM 的文件。
- 按套餐计费且不重复计费:配置里的 subscriptions 填月度套餐(
{ vendor, plan, amount, currency, startedAt?, endedAt?, note? }),月费会按账单覆盖的
天数摊分——¥199 的套餐算到 1 日至 10 日就是 ¥66.33,不是 ¥199。上了套餐的厂商按套餐计费,
套餐覆盖掉的用量在它旁边单独显示而不是相加:这两个数分开正是为了不把同一批调用算两遍。
套餐是一笔钱,所以在「同一厂商出现在多行」的分组里(工作区 / 会话 / 模型),它按各行用量
占比摊分到这些行上,这样每个分组的各行之和就等于该分组的合计;被套餐摊到的行会在费用
下面写明「其中套餐摊分 ¥…」。区间内完全没有用量、但确实买了套餐的厂商照样计入,因为钱确实花了。
套餐的 vendor 既可以写提供商、也可以写计价厂商(bos 或 deepseek),两种都会落到它覆盖的用量上。
- 算不出价的不按 0 计:每个未定价的模型会连同 Token 数、调用次数和原因一起列出——
该模型没有价格、同名模型被两家同时公布(不敢猜是哪家)、或价格币种无法换算。
靠名字(而非 id)匹配上的价格也会列出,方便核对而不是盲信。
- 按时段报价的厂商按时段结算:DeepSeek 高峰与空闲价格不同,账本记录了每次调用落在
哪一侧——北京时间周一至周五 9:00-12:00、14:00-18:00 为高峰——于是两边各按自己的价格算,
而不是统统按其中一边。
- 账单是估算,并且写在页面上:它是「会话日志记下的 Token × 路由真正指向的那个模型
的公开价目」。它不代表厂商最终开给你的账单——失败重试、部分返回的请求都会不一样;
套餐自带的额度也只按你在配置里填的来。
页面读三个仅限回环的路由:概览用 GET /api/token-ledger/summary,价格与汇率用
GET|POST /api/token-ledger/rates,账单用 GET /api/token-ledger/bill。
概览每分钟轮询一次,价格与账单只在切到各自页面时才拉取。
三个路由都会拒绝非回环来源,因此即使 web 服务器绑定到 0.0.0.0 也不会外泄;
写入那半边还额外要求 JSON 内容类型(跨站表单发不出这种类型),并把请求体限制在 256 KiB。
它需要带 web 服务器的 profile(web 或 desktop)。没有的话 /tokens 与 CLI
照常可用,分区会明确说明而不是直接失败。注意手工填价格是在那个页面上填的:目前
还没有等价命令行入口,所以 headless profile 能读用量账本,但没法录入价格。
三个视图都刻意不通过设置命名空间下发:那需要 schema(一个真实依赖,而本包零依赖),
而且每次防抖都会用可推导、可重放的数据重写一遍 settings.yaml。
账本文件始终是唯一的记录来源,价格存在它旁边的 rates.json 里。
配置
按 id 覆盖组合条目:
- id: token-ledger
config:
ledgerPath: 'D:/dsh/ledger.json' # 默认 /token-ledger/ledger.json
backfill: false # 默认 true——启动时折叠已存历史
rates: false # 默认 true——false 时完全不发网络请求
rates 也可以写成对象:
rates:
source: modelsdev # 默认按厂商数据集;改成 openrouter 则用网关报价
refreshIntervalMs: 1800000 # 默认 30 分钟
perVendor: 3 # 默认每厂商取最新 3 个
vendors: 15 # 默认最多列几家厂商;填 0 则列出全部
modelsUrl: 'https://…' # 默认随 source——https://models.dev/api.json
fxUrl: 'https://…/latest/USD' # 默认 open.er-api.com
月度套餐,供账单页的「按套餐」分组使用。每个套餐按账单覆盖的天数摊分,
因此不满一个月就只计一部分。
subscriptions:
- vendor: deepseek # 这个套餐覆盖哪家厂商的用量
plan: 'DeepSeek 包月' # 账单上显示的套餐名
amount: 199 # 每月费用
currency: CNY # 该费用的币种
startedAt: '2026-09-01' # 可选:这天之前不计
endedAt: null # 可选:这天之后不计
note: null # 可选:备注,随套餐一起显示
页面如何缩短「会话」列表。导出从不缩短,所以这三项只影响页面上显示什么。
bill:
foldSmallSessions: true # 默认 true;设为 false 则页面上也列出每一个会话
smallSessionCost: 1 # 默认 1,按账单当前币种
smallSessionCalls: 10 # 默认 10
rates: false 会彻底关掉定价功能的联网,只保留手动填写的值——这正是严格离线环境
需要的配置。费率页本身照常可用。
它刻意不做的事
- 概览页给出写明口径的预计花费,账单页给出逐行明细。 概览页在区间总计与今日实时里各显示
一个预计花费(人民币、两位小数),用的就是账单那套算法;账单页则给出每个分组、每个时间段
的逐行费用。两者都会写明价格来源、换算用的汇率、每一个靠名字匹配上的价格,以及每一个没能
定价的模型,因此这些数字是用来核对的,不是用来信的。它们都不是厂商开给你的账单,
也不声称是——具体漏了什么见上面的「账单」一节。
- 不内置价目表。 价格要么从一个可选的源联网取(默认是各家官方价目),要么你自己填。
内置一份价目表几周内就会过期,而且每次更新都得发一个版本。
- 不注册面向模型的工具。 工具 schema 会在每次请求上花提示词 token 并改变缓存前缀——
对一个 token 记账插件来说这很荒唐。人类场景由 /tokens 与 CLI 覆盖。
兼容性
- Node: ≥ 22.15.0(CLI 需要解码 Zstandard 帧)。宿主端本身没有版本相关要求。
- DSH: ^0.1.2-rc.1,以对五个宿主包的可选 peer 形式声明——见
需要的 DSH 版本。在 0.1.2-rc.1 上验证过。所用到的接口面——
ctx.on、ctx.inject、ctx.get、ctx.effect、commands.register、
sessionPersistence.list()/inspect()——在 0.1.2 线上一致。
- Profile: 任意。没有 profile 相关代码。
- 网络: 费率页会通过 HTTPS 取价格与美元汇率,但这完全是可选的:没有外网时仍然
提供本机缓存的上次结果,手动填写的值照常可用,rates: false 则连请求都省掉。
卸载
dsh plugin --profile web remove @chenmiao8563/dsh-token-ledger
账本文件是故意不删的——要清空历史请自行删除 /token-ledger/。
开发
npm install # 两个 devDependency:react 与 react-dom,供渲染测试使用
npm test # 288 个测试,14 个文件
npm run verify # 打包不变式(零依赖、无安装脚本、无裸模块说明符)
npm test 使用 Node 内置测试运行器。在禁止逐文件 spawn 子进程的受限环境里,
改用 npm run test:single-process。
React 只是 devDependency,消费者永远不会安装它:本包不带任何运行时依赖、不带任何
安装脚本,声明的 peer 全是可选声明、没有任何东西会去抓取,而 pnpm 不会为依赖安装其
devDependencies。它在这里的作用是让
浏览器端能用真库渲染并断言——这能抓到替身抓不到的东西:hook 顺序违规与非法
DOM 属性,React 会报出来,而手写的 createElement 会默默接受。没装它时这些渲染
测试会带明确原因跳过而不是失败,所以全新克隆、无网络也能跑 npm test。
改动的生效方式。 两半都是在插件挂载时读取的,所以改 lib/index.js 或
lib/client.js 都需要重启 DSH(或重载插件)——刷新页面不够,因为 bundle 的 rev
在挂载时算定,URL 没变浏览器就继续用缓存那份。这是量出来的不是猜的:在隔离 host
运行时,改 lib/client.js 之后被服务的 bundle 与它的 rev(d683dd523466)都纹丝不动;
重启后 rev 变成 f55ee321db50 并提供了新内容。导出的是文件,所以如果你手上的那份
来自某次修复之前,看时间戳就知道。
实际验证了什么、怎么验证的(包括 fork 规则背后的证据)见
docs/VERIFICATION.md。
发布
打一个 tag 就是发布。不需要在任何地方存放 token。
git tag v1.0.0 && git push origin v1.0.0
release.yml 会跑测试与打包检查、确认 tag 与 package.json 一致、用 OIDC
trusted publishing 发布、把 tarball 挂到 GitHub Release 上,并在报告成功之前
断言该版本确实已在 registry 上——所以绿色意味着已发布,而不只是尝试过。
最后这条断言不是装饰。这条流水线从 v0.5.0 到 v0.8.0 每个 tag 都失败,报的是
404 Not Found - PUT——它说的是"包不存在",而真正的问题是缺凭据;而且失败之后
registry 会有几分钟看起来是空的,尽管发布其实已经成功。两个原因都已在 workflow 里
加了防线:
- npm CLI 必须 ≥ 11.5.1。 Node 22 自带 npm 10,根本做不了 OIDC 交换。workflow
跑在 Node 24 上,并把版本打进日志。
- 不能用 actions/setup-node 的 registry-url 输入。 它会往 .npmrc 写
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN},npm 看到这一行就认定
"已有凭据",直接跳过 trusted publishing,报 ENEEDAUTH——在一个本就不持有凭据的
任务里,这读起来像"你忘了登录"。发布前现在有一道步骤:只要 .npmrc 里出现
_authToken 就大声失败。
npm 那边要接受它,需要在 npmjs.com 上给包配 Trusted Publisher(包 → Settings →
Trusted Publisher → GitHub Actions),仓库填 chenmiao8563/dsh-token-ledger、
workflow 填 release.yml、Environment 留空——填了一个 workflow 没有声明的
environment 就匹配不上,registry 只会回一句
OIDC token exchange error - package not found。
在 trusted publishing 出现之前,一个版本的首发是用
npm publish --access public --otp= 手动发的,因为 OIDC 没法在包
存在之前配置。那条路现在依然可用,也依然需要带 2FA bypass 的 granular token,
但它不产生 provenance 证明,而且 v0.8.0 那次排查正是从这条路走起——从一个过期的
token 开始。优先用打 tag。
重复运行是安全的:版本已在 registry 上时跳过发布步骤,GitHub Release 只在不存在时创建。
许可证
MIT