DeepSeek Harness 插件

chenmiao8563/dsh-token-ledger

Star 数 ★ 1 下载量(近 30 天) 1,667 分类 用量与计费 收录于 2026-09-21 npm @chenmiao8563/dsh-token-ledger

可审计的 DSH token 账本:把持久化会话日志折算成重启安全的总量,可从原始日志重算并比对差异,并按工作区、会话、模型或厂商生成账单,可导出 CSV 或 JSON。

安装

# npm 包(预构建)

dsh plugin --profile web add @chenmiao8563/dsh-token-ledger

# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)

dsh plugin --profile web add github:chenmiao8563/dsh-token-ledger

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWEDERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yamlallowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。

README

DeepSeek Harness 提供透明、可审计的 token 记账。

CI

English | 中文


这是什么

这是一本账本,不是一块仪表盘。它把 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 写到 <DSH_HOME>/token-ledger/exports/
/tokens json       # 原始快照
/tokens path       # 账本文件位置

账本写在 <DSH_HOME>/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 <path>    DSH home(默认 $DSH_HOME,其次 ~/.dsh)
--ledger <path>  要读写的账本文件
--out <path>     导出目录
--days <n>       摘要显示天数(默认 7)
--models <n>     摘要显示模型数(默认 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 与路径放在悬停提示里);模型一行显示 提供商/模型——就是账本记录的那条路由, 与概览页「按模型」列出的名字完全一致——具体按哪份价目计价放在悬停提示里。
  • 供应商一行是「你接入的提供商」,不是一份价目表bosqwen-plandeepseek-officialzai 是接入的端点,deepseekqwenz-ai 是这些 Token 按谁家的 价目计价。所以这一行显示的是提供商——用设置页给它的显示名(BOS-API 而不是 bos, 这个名字是从 harness 的 settings 文件里出来的,绝不写入),下面再写明计价来源 (计价来源 deepseek)。因此同一个模型经由两个端点调用,就是两行——这正是供应商账单要回答的问题。
  • 每个分组五个时间段:本月 / 本年 / 7 天 / 今日 / 全部,与概览页同一套定义。
  • 会话列表在页面上汇总,导出的文件不汇总:只问了一句就归档的会话不值得单独占一行,而一台机器会攒下几百个, 所以花费很少或几乎没用过的会话——低于 ¥1,或调用少于 10 次——在页面上合并成一行 (其余 N 个低频会话,带「汇总」标记,各列是它们的合计)。总额一分不动:各组行之和仍等于分组合计。 表格下方会写明汇总了多少个会话、规则是什么,并说明导出永远是完整明细——format=csv 里每个会话都在。 两种情况永不折叠:算不出价的会话,以及整张表每一行都符合条件时(那种情况下列表本来就短,折叠反而把列表藏起来)。 bill.smallSessionCostbill.smallSessionCallsbill.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 既可以写提供商、也可以写计价厂商(bosdeepseek),两种都会落到它覆盖的用量上。
  • 算不出价的不按 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(webdesktop)。没有的话 /tokens 与 CLI 照常可用,分区会明确说明而不是直接失败。注意手工填价格是在那个页面上填的:目前 还没有等价命令行入口,所以 headless profile 能读用量账本,但没法录入价格。

三个视图都刻意不通过设置命名空间下发:那需要 schema(一个真实依赖,而本包零依赖), 而且每次防抖都会用可推导、可重放的数据重写一遍 settings.yaml。 账本文件始终是唯一的记录来源,价格存在它旁边的 rates.json 里。

配置

id 覆盖组合条目:

- id: token-ledger
  config:
    ledgerPath: 'D:/dsh/ledger.json'   # 默认 <DSH_HOME>/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.onctx.injectctx.getctx.effectcommands.registersessionPersistence.list()/inspect()——在 0.1.2 线上一致。
  • Profile: 任意。没有 profile 相关代码。
  • 网络: 费率页会通过 HTTPS 取价格与美元汇率,但这完全是可选的:没有外网时仍然 提供本机缓存的上次结果,手动填写的值照常可用,rates: false 则连请求都省掉。

卸载

dsh plugin --profile web remove @chenmiao8563/dsh-token-ledger

账本文件是故意不删的——要清空历史请自行删除 <DSH_HOME>/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 与它的 revd683dd523466)都纹丝不动; 重启后 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-noderegistry-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.ymlEnvironment 留空——填了一个 workflow 没有声明的 environment 就匹配不上,registry 只会回一句 OIDC token exchange error - package not found

在 trusted publishing 出现之前,一个版本的首发是用 npm publish --access public --otp=<认证器里的 6 位数字> 手动发的,因为 OIDC 没法在包 存在之前配置。那条路现在依然可用,也依然需要带 2FA bypass 的 granular token, 但它不产生 provenance 证明,而且 v0.8.0 那次排查正是从这条路走起——从一个过期的 token 开始。优先用打 tag。

重复运行是安全的:版本已在 registry 上时跳过发布步骤,GitHub Release 只在不存在时创建。

许可证

MIT

内容来自项目 README(GitHub)↗

链接

同类插件

查看整个分类 →

社区评论

评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。