安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add @wingsky-1/dsh-provider-usage
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
README
DSH(DeepSeek Harness)Web GUI 插件:多 provider 通用用量统计框架(v2 适配器契约)。
聊天界面右上角常驻悬浮胶囊,展示当前模型 provider 的用量信息;点击展开详情面板。
内置两套适配器开箱即用——DeepSeek 官方(余额 + 峰谷倒计时徽标 + 每日用量推算)与
OpenCode Go(官方 /v1/usage 三窗口用量);其他任意数据源只需按 v2 契约写一个
mjs 适配器文件即可接入(设置页检测/添加/切换热插拔,见「适配器开发指南」)。
渲染在宿主端完成——适配器返回 HTML、客户端只做注入,密钥不进浏览器。
一键安装
dsh plugin --profile web add @wingsky-1/dsh-provider-usage
安装 / 卸载 / 更新后都需重启一次
dsh web(bundle 层只在启动时组合)生效。
核心优势
- 通用框架 + 开箱即用:一套 v2 适配器契约承载任意 provider;DeepSeek 官方与 OpenCode Go 两套适配器内置,装完即显示用量
- 接入任意数据源只需一个 mjs 文件:
fetchData/formatCapsule/formatPanel三个导出即完成接入;设置页检测/添加/切换热插拔,改文件自动热更新 - 官方没有用量接口也能算:DeepSeek 内置适配器以区间记账法推算每日消耗 (纯消费区间 = 余额降幅,可与平台账单对账;充值独立列示不混算;异常区间不计), 余额折线充值时刻自动断轴平移,附峰谷倒计时徽标与 15 日用量柱形面板
- 密钥不出宿主:取数在宿主端执行,密钥只在宿主端持有与使用、不下发浏览器
(推荐凭据链 / env 注入;显式
apiKey配置会随宿主配置落盘并以 0600 保护); 渲染输出双层净化(esc()转义义务 + 结构化净化兜底),XSS 双重防线 - 历史可回溯、性能有兜底:按天分片 JSONL 落盘(0600)、超龄超量自动清理; 面板渲染进程内缓存 + stats 缓存 + 后台预热,数据未变时重复请求零重算
安装
前提:已安装 DeepSeek Harness 且 dsh web 可正常启动(未全局安装 dsh 见下方「未全局安装 dsh」)。
安装插件(add)
dsh plugin --profile web add @wingsky-1/dsh-provider-usage
卸载插件(remove)
dsh plugin --profile web remove @wingsky-1/dsh-provider-usage
更新插件(update)
dsh plugin --profile web update @wingsky-1/dsh-provider-usage
安装 / 卸载 / 更新后都需重启一次
dsh web(bundle 层只在启动时组合)生效。
未全局安装 dsh
若本机没有全局 dsh 命令,用 npx 临时拉起:
npx @deepseek-ai/dsh plugin --profile web add @wingsky-1/dsh-provider-usage
工作原理(v2)
宿主端(Node) 客户端(浏览器)
───────────────────────────── ─────────────────────
预热定时器(5min) ─┐
├→ getStats() 60s 轮询 /stats(取数前先复检
客户端轮询 ───────┘ │ Mutex 互斥锁 会话当前 provider,#71 自愈)
│ 60s 缓存 胶囊框架 ← capsuleHtml
│ 5s 取数超时 面板框架 ← panelHtml(/history,90s 兜底缓存)
↓
adapter.fetchData(ctx) ← 用户 mjs(apiEndpoint/staticPath/apiKey 注入)
↓
按天分片 JSONL 历史落盘 ──→ 面板渲染缓存全清(主失效)
↓
adapter.formatCapsule/Panel() → 净化 → HTML 下发
内置适配器 opencode-go-builtin(OpenCode Go 官方 /v1/usage 三窗口用量)与
deepseek-official-builtin(DeepSeek 官方余额 + 峰谷倒计时徽标,见下节)开箱即用。
provider 跟随语义(0.1.2 投影面,#383):切换会话即时刷新;会话内切模型经宿主 modelSelection 投影帧(control frame type:projection)实时推送、客户端切完即重拉; 信号缺失最坏场景由每次取数前的复检兜底自愈(#71)——最长一个轮询周期内收敛。
图解文档:完整的流程图 / 时序图(启动装配、
/stats取数全链路、/history面板、自定义适配器注入三条路径与热更新、客户端交互、密钥解析链)见 docs/architecture.md。
配置
| 键 | 默认值 | 含义 |
|---|---|---|
enabled |
true |
插件开关 |
adapter |
无 | 用户适配器 mjs 路径(缺省用内置 opencode-go) |
provider |
opencode-go |
关联的模型 provider 名 |
staticPath |
无 | API 路径(注入 fetchData 入参,与 apiEndpoint 拼接) |
apiEndpoint |
无 | API 基础地址(可选;显式配置优先于凭据链) |
apiKey |
无 | 显式密钥(可选;缺省走凭据解析链,不进设置面板回显) |
historyDir |
<DSH_HOME>/dsh-provider-usage/ |
历史存储根目录 |
warmupIntervalMs |
300000 |
后台预热间隔(无客户端访问时保持历史连续) |
cacheDurationMs |
30000 |
缓存新鲜度(毫秒,下限 5000;#198 由 60000 下调——峰谷徽标跨时段边界端到端翻转延迟 ≤95s = 宿主缓存 30s + 客户端轮询 60s + 渲染余量) |
fetchTimeoutMs |
5000 |
fetchData 强制超时(固定值,不可配置;#208 起 2s→5s,用户配置不生效) |
autoReload |
true |
热更新开关(编辑适配器文件后自动加载;默认开启,可显式 false 关闭) |
maxAgeDays |
30 |
历史保留天数 |
maxSizeMB |
20 |
历史大小上限(MB,超限从最旧日文件删) |
trendRetentionDays |
180 |
会话用量趋势聚合保留天数(#503;日切压实后按天留存,正整数上界 3650) |
报告配置(historyDir/reports/config.json,设置页「报告」tab 承载)
| 键 | 默认值 | 含义 |
|---|---|---|
daily / weekly / monthly |
全关 08:00/09:00/09:00 |
三周期独立开关与触发时刻(weekly 另有 weekStartsOn 周一起点、monthly 另有 dayOfMonth 触发日) |
provider / model |
""(跟随默认) |
报告生成所用模型路由(空串 = dsh 注册序首个) |
reasoningEffort |
未设置 | 当前 exact model 的 DSH reasoning effort opaque ID;未设置时请求省略该字段并沿用 DSH 默认。设置值必须存在于该模型的能力响应中,否则在发起模型请求前 fail closed;档位顺序、名称与默认值不复制、不按 low/off 或数组末档推断 |
prompts |
三周期年报模板 | 三周期独立提示词({stats} 占位;#633 起默认模板含目录观察——目录名 basename、占比分母 totals.total、只报数字不解读目录内容;#662 起默认模板再含时段观察——钟点/时段档只可原样引用 byHour/byPeriod/peakHour 字段、禁行为脑补、禁与目录交叉关联;存量旧默认模板读时自动升级) |
push.enabled |
false |
生成完成后经 dsh-notifier 推送摘要(摘要仅周期/窗口/总量/调用数数值,不含项目路径) |
directories |
[](全部) |
#633 目录范围:非空数组(目录 basename 列表,"all" 显式全选语义,至多 32 项)= 报告统计只呈现所选目录的目录分布;空数组 = 全部目录。影响报告生成统计口径(byDirectory 维度按所选目录过滤) |
报告生成、推理等级与有界重试
- reasoning effort:客户端只展示并原样保存当前 exact model 的 DSH 能力响应;未选择时省略
reasoningEffort,显式 ID 在生成前按同一 exact model 校验。能力暂不可用时保留已有值并显示提示,不静默清空,也不回退到low/off。 - 预算与退避:初次报告调用之外最多自动 retry 5 次,
attempts=0..5;第 1 至第 5 次自动重试分别退避 1、2、4、8、16 分钟。只有可分类的 transient 与空输出(reasoning-only / 双空)进入该预算;认证、额度、非法请求、content-filter、abort、未知终态与 unsupported block 均 fail closed,不自动重试。 - attempt 口径:初次调用 + 自动 retry 最多形成 6 个报告级 outer attempt,每个 outer attempt 至多 dispatch 一次报告 stream。插件直接消费
ctx.llm.stream,不经过 DSH agent request retry waterfall;因此这只是报告级预算,不等于也不保证底层 HTTP 请求恰好 6 次。 - 状态与恢复:逻辑身份是
period + report key。读取会在内存中按 period 规范化:所有非终态 entry 保留在records,每个 period 只保留按 key 排序后的最后一条 terminal entry;写盘序列化时再次裁剪,并把被裁剪的 terminal key 写入全量terminalKeys(period → key → 终态 code,按 key 排序且不按数量淘汰)。墓碑只保留终态 code,不携带 attempts、observations 或 usage;所有已知 terminal key 都保留墓碑,避免历史 key 被自动重开。flatten以及list/listDue/get只投影records,不会把墓碑展开成完整 entry。没有 entry 且没有墓碑的 key,自动beginAttempt才会创建 initial entry;已有 entry 时必须带匹配的cycleId,waiting 还必须已到nextRetryAt,in-flight 或 terminal 返回null;若records没有该 key 但terminalKeys有墓碑,也返回null。对这个只剩墓碑的 key,手动「重新生成」(force) 才会清除墓碑并创建新 cycle、重置计数、重新读取当前配置;force 失败仍不推进 lastRun。 例如daily中2026-09-21和2026-09-23都 terminal 时,records.daily只保留2026-09-23的完整 entry,terminalKeys.daily记录2026-09-21: "auth-failed";对被裁剪的2026-09-21调用beginAttempt返回null,只有beginForce才会移除墓碑并开始新 cycle。 - 累计成本:每个实际报告 attempt 分别记录 input/output/reasoning/total/cache-read/cache-write token 与 duration;
ReportTokenUsage.reasoningTokens和 retry usage 均为number | null。周期累计按字段独立聚合,任一 attempt 缺失某字段时该字段保持null,不把未知伪装成 0。设置页状态区显示当前 outer attempt、累计成本、下次重试或 terminal 原因;旧状态响应没有retry时保持原 UI。 - 耐久性边界:retry ledger 自身使用 0600 临时文件、完整写入、文件 fsync、原子 rename 与支持的平台上的目录 fsync;损坏文件先 no-clobber 隔离取证,再 fail closed,不把损坏当空账本。现有 report/index/lastRun 全链尚未全部 fsync,因此这里只承诺 ledger 自身耐久与进程崩溃/重启恢复,不宣称掉电下全链原子 durability。
启用选择状态恢复
historyDir/adapter-state.json 保存 provider → 启用适配器的映射。合法状态通过独占临时文件、
文件 fsync 与原子 rename 写入;支持目录 fsync 的平台还会同步父目录。rename 前的读取或写入
I/O 错误会取消本次持久化,避免覆盖无法读取的旧状态。rename 已成功而父目录 fsync 失败时,
新状态已提交并继续生效;health / 日志会单独提示「崩溃后的耐久性未完全确认」,不会误报成
写入失败。
坏 JSON 或非法顶层形态会以 no-clobber 方式移出主路径,保存为
adapter-state.json.bak-<ts>[-n],随后本次启动按默认启用关系继续。该 .bak 仅供取证、
不会自动恢复或合并;后续合法选择会从当前默认/运行时状态重新落盘。为避免外部反复损坏
造成无限累积,正常轮转最多保留 5 份取证备份,并始终保护本次新隔离的现场。因此
fail-closed 仅适用于旧状态无法读取或隔离失败的路径,不适用于已成功隔离的坏 JSON/非法形态。
胶囊位置配置
用量胶囊(会话右上角悬浮球)与面板的位置支持自定义:打开设置 → 插件 →「用量统计」→「胶囊位置」区,选择锚点(右上 / 左上 / 右下 / 左下)与偏移(水平 / 垂直 / 面板间距 / 层级基准)后点「保存」——立即生效且跨设备同步(宿主落盘 ui.json 并经 SSE 广播,无需重启)。默认值:右上 / 0 / 48 / 10 / 40——胶囊采用固定定位,不随会话滚动内容滑动位移(无避让抖动,滚动时位置稳定);水平偏移 0 使胶囊右缘贴近容器右缘(右侧对齐);垂直偏移 48 让胶囊默认位于 MCP 管理器浮窗(同为右上角、距顶 8px)正下方,两胶囊默认互不重叠;面板间距 10px;层级基准 40 对应 CSS 默认 z-index: 40,胶囊与点击后弹出的主面板同取该配置值(#128 重开维护者要求,不再派生 +30)。胶囊与 MCP 浮窗互不探测、互不避让,各自位置只由本插件配置决定。
| 键 | 值域 | 默认 |
|---|---|---|
placement |
top-right / top-left / bottom-right / bottom-left |
top-right |
offsetX / offsetY / panelOffsetY |
非负整数,clamp 到 0–2000(单位 px) | 0 / 48 / 10 |
zIndexBase |
整数,clamp 到 1–9000(胶囊与点击后弹出的主面板同取该配置值) | 40 |
移动端 / 平板端适配(issue #128):断点判定基准是会话容器(conversationHost)
的视口宽度而非窗口媒体查询——窄屏(≤480px,手机竖屏 / 极窄分栏)下面板近全屏宽、
卡片重排、按钮触控目标加大到 ≈44px;平板档(≤834px)过渡;桌面维持现状。
胶囊最终坐标经 JS 视口 clamp(safe-area 语义:宿主无 viewport-fit=cover,
env(safe-area-inset-*) 恒 0 时自然退化为普通 clamp);软键盘弹出经
visualViewport resize 跟随,横竖屏切换后下一帧重算。
跨包避让契约(源自 issue #116,不可回退):本插件默认 offsetY: 48 依赖
dsh-mcp-manager 浮窗的默认位置(top-right、距顶 8px、高约 26px)在其正下方
让位;修改该默认值前须同步评估 mcp-manager 默认锚点 / 偏移,回退属跨包行为
契约变更,两包须联动调整。
DeepSeek 官方内置适配器(deepseek-official-builtin)
认领 provider deepseek-official,对接 DeepSeek 官方「查询余额」接口
GET https://api.deepseek.com/user/balance(来源:
api-docs.deepseek.com/api/get-user-balance)。
数据口径
- 仅保留 CNY 币种:官方
balance_infos[]含多币种条目时只取currency === "CNY"一条, 其余(如 USD)全量忽略;金额自官方字符串字段严格解析(非法/缺失 →null,杜绝 NaN 落盘)。 - 无 CNY 条目(仅 USD 或空数组)时产出
balance/toppedUp/grantedBalance = null的正常帧 (不抛错),胶囊显示「DeepSeek 余额 --」占位。 is_available=false表示账号不可用:该帧仍记录与展示余额,但不参与每日消耗的区间记账 ——相邻区间的推算跳过并在面板标注「含不可用区间不计」,避免把封禁/清零误计为消耗。
每日用量推算(区间记账法)
官方 API 无任何用量接口,每日消耗由相邻采样点逐区间记账推算。字段语义前提(实测确认):
topped_up_balance 是充值账户当前剩余(恒等式 total = toppedUp + granted 成立,
消费时 toppedUp 与 total 同步下降),故不做任何代数相消,直接按区间性质分类:
| 相邻采样区间 | 判定 | 处理 |
|---|---|---|
toppedUp 无增加且 granted 不变 |
纯消费区间 | 消耗 = 余额降幅,可与平台账单对账 |
toppedUp 上涨 / granted 变动 |
扰动混合区间 | 消费漏计;提取「充值 +¥X」事件独立列示 |
| 跨度 > 27h(采样中断)/ 任一端不可用 | 跳过区间 | 不计柱不计入汇总,面板注明 |
- 区间归属:计入结束端所在日——跨午夜隔夜消费不丢失;当日有 ≥1 个完整区间即出数 (冷启动自然成立,无跨日基线依赖)。
- 展示分层:日柱 = 当日落账区间降幅之和;充值合计在汇总行独立展示为绿色 「另有充值 +¥X」,绝不与消耗混算;卡1 徽章同口径(近 24h 纯消费区间求和)。
- 折线断轴(B2):充值时刻做断轴平移——充值后各点按累计充值额整体下移抹平台阶, 断轴处画虚线连接两侧真实水位并注明金额,跳变显式可见可回溯。
双卡面板与峰谷徽标
- 卡1:CNY 余额大头 + 消费徽章(区间记账口径,充值不误报为 ▲)+ 近 24h 波动折线 (统一时间锚、降采样 ≤300 点、充值时刻断轴平移)。
- 卡2:近 15 个自然日每日消耗柱形图(区间记账口径;消耗蓝柱向上、净增绿柱向下、 异常仅标注;充值额在柱 title 与汇总行独立列示)。
- 胶囊常驻峰谷倒计时徽标(纯本地时间计算,不依赖远端数据——取数失败时同样显示):
- 时段定义为 UTC 工作日固定窗口
01:00–04:00 / 06:00–10:00(半开区间), 来源 api-docs.deepseek.com/quick_start/pricing, 核实日期 2026-08-26;硬编码常量无配置项,周末全天低谷。 - 谷态显示距下次开峰倒计时(如
⚡谷 · 距峰 02:41),峰态显示距切谷倒计时; tooltip 注明 UTC 时段定义与服务器时区对照。
- 时段定义为 UTC 工作日固定窗口
密钥解析顺序(V1 配置链)
- 插件配置
apiKey(显式指定) - 环境变量
{PROVIDER}_API_KEY(大写,连字符换下划线) - opencode-go 兼容旧环境变量
OPENCODE_GO_API_KEY <DSH_HOME>/.credentials.yaml的{PROVIDER}_API_KEY(opencode-go 在标准 key 未命中时再查旧名OPENCODE_GO_API_KEY)- opencode-go 兼容:
~/.local/share/opencode/auth.json的opencode-go(或opencode)条目
DeepSeek 官方适配器三级密钥链:插件配置
apiKey注入 → 凭据链推导 env (providerdeepseek-official→DEEPSEEK_OFFICIAL_API_KEY)→ 适配器内自查DEEPSEEK_API_KEY兜底(与 llm 层共用,覆盖「llm 能跑、余额接口 401」场景; 该兜底属适配器实现细节,不在共享 provider-config 层特判)。三级全空时取数报no-api-key并降级 stale 帧(峰谷徽标仍渲染)。
路由(全部 loopback 围栏)
| 路由 | 说明 |
|---|---|
GET /api/dsh-provider-usage/stats?provider=X |
用量统计 + capsuleHtml(胶囊内容)+ status/adapterVersion |
GET /api/dsh-provider-usage/history?provider=X&days=N |
历史查询 + panelHtml(面板内容)+ 查询 range(进程内渲染缓存,见下节) |
GET /api/dsh-provider-usage/trend?granularity=day&metric=total&n=30&provider=X&byModel=1&dir=Y&byDir=1 |
会话用量趋势(#503 M2);#633 起支持可选 dir 目录过滤(目录 basename 或 (unidentified),非法值回退全目录聚合;传 dir 时响应按目录维度拆段并附 dirs 目录图例,未传时形状与 #633 前一致);#633 复核闸起支持 byDir=1 全目录拆段面(未传 dir 时按目录拆段 + dirs 全集图例,加性附 providers 适配器候选;dir 优先于 byDir,同传时按 dir 过滤面生效并回显) |
GET /api/dsh-provider-usage/health |
健康检查 + 适配器快照 + 错误登记 |
GET /api/dsh-provider-usage/adapters.json |
适配器候选元数据(设置页主列表同源,含 modelProviders) |
POST /api/dsh-provider-usage/adapters/select |
切换/清空启用适配器 |
POST /api/dsh-provider-usage/adapters/inspect |
预览适配器文件(回显导出信息,不注册) |
POST /api/dsh-provider-usage/adapters/add |
登记用户适配器文件(设置页承载) |
趋势目录维度(数据口径)
趋势面板的目录维度回答「用量花在哪个工作目录」:目录归属取自会话创建元数据
SessionHeader.cwd(官方契约字段),落盘前经 sanitizeDirName 归一化为 basename
(剥 C0/C1 控制字符;POSIX / 与 Windows \ 分隔符同取;根路径/空值 → 未识别桶)。
- 未识别桶(
(unidentified)):会话无 cwd、归属获取失败,或该日数据产生于 目录维度上线之前(旧分片没有目录信息)。桶不静默丢弃,UI 与报告照常呈现。 - 总量守恒:正常数据下目录面与 provider 面的日总量恒等。目录面 = 已记录的 目录日桶 + 每日残差(聚合面 − 目录面,归未识别桶)——残差即「该日无目录信息 的数据」,因此历史用量不会因为缺少目录字段而从趋势图消失,也不会与目录行重复计数。 残差为负(目录面反而多于聚合面)属分片数据异常,此时按 0 处理、不产生负值, 恒等关系不成立(该异常已由明细分片读白名单阻断主要来源)。
- 只读投影:残差在查询时计算,不写回分片、不改动既有数据文件。
- 不可恢复的边界:目录信息在会话首次记账时确定,历史分片无法回溯推断;故 升级前的数据在目录维度恒为未识别桶,只有新产生的用量才会出现真实目录名。
趋势时段维度(数据口径)
报告的时段叙事(#662)回答「用量集中在哪个钟点」:日切压实在 agg/dir 之外同源
产出 day×hour 聚合行(分片 kind:"hour",hour 为本地时区 0–23,与 dayKey 同源
口径——同一事件按同一本地时区归日与钟点,DST 逐时回退安全)。报告快照注入
byHour[24] / byPeriod[4](凌晨 0-5 / 上午 6-11 / 下午 12-17 / 晚间 18-23)/
peakHour,供提示词写「最常开工的钟点」等叙事。
- 覆盖度守卫:快照带
coveredDays(窗口内有 hour 事实的天数)。升级期窗口内 只有部分天有 hour 行时(coveredDays < windowDays),三个时段字段整体置 null、 提示词时段段整段降级——杜绝「局部天代表全窗口」的误导叙事。 - 落盘即定型:hour 值只在折算点(日切压实折算,现算自明细行 time)产生; 分片读回只信落盘字段、绝不重算(防时区配置变更导致旧行漂移)。
- 无残差投影:明细/计数行必有 time、无缺键事实;升级前历史分片缺 hour 行是 物理缺失(不可回溯),由覆盖度守卫降级,不投影补造。
- 版本回退代价:
TREND_ROW_VERSION保持 1(加性扩展)。若插件回退到 #662 前 的版本,旧版压实会整日重写聚合分片、抹掉 hour 行;再升级后该日小时数据不可逆 丢失(agg/dir 主数据不受影响),报告端由覆盖度守卫降级兜底。 - 口径边界:时段与目录是两个互斥查询面(hour 行无 dir 关联);提示词红线禁止 把时段与行为/场景关联(如「凌晨还在写代码」的「写代码」不在 JSON,属编造), 也禁止 byPeriod/byHour 与 byDirectory 交叉关联(两口径不同)。
/history 渲染缓存
/history 的 panelHtml 在宿主进程内缓存(issue #105 子项①),数据未变时重复请求零重算:
- 命中条件:同进程内
(provider, 启用适配器, 归一化查询窗口)三者均未变。窗口按 自然日粒度归一化——end=Date.now()的请求间漂移不参与 key,同一自然日内的重复 请求命中同一条目;不同days参数归一化为不同条目、互不串数据。命中回放只复用返回值 字符串层快照({panelHtml, error, at}),绝不缓存 entries 中间层;响应中的range仍回显本次请求的真实 start/end。 - 失效时机(四处):主失效 = 历史采样落盘成功时全清(新数据已入库,所有面板条目 一次性失效);此外 select(切换/清空启用适配器)、add(登记新适配器)、 热更新(适配器文件变更加载成功)三处挂点同步全清。
- 兜底 TTL:编译期常量
90000ms(90 秒,定界 [60s, 120s] 区间取中值),非配置键; 仅作兜底而非主失效机制——生产命中率由 warmup 周期(5min)与 stats 缓存 TTL(60s) 复合门控:两次落盘之间的客户端轮询全部命中。 - 不缓存边界:管道错误响应与无适配器/无启用适配器的结构化响应一律不入缓存——条件 消除后下一次请求立即重算,不会在剩余 TTL 内复读旧错误或旧占位结构。
- 无条件请求协商:响应不含 ETag / Last-Modified,不做 304 短路;客户端维持
cache: "no-store",本缓存为纯服务端行为、客户端零改动。
适配器开发指南(v2 契约)
写一个 mjs 文件即可接入任意数据源(参考实现见内置适配器源码 src/adapters/opencode-go.mjs、
src/adapters/deepseek-official.mjs、src/adapters/zai-coding-cn.mjs;宿主端在
fetchData/formatPanel 入参注入共享图表工具 utils(见 docs/adapter-guide.md §3.3);
agent 导向的接入手册见 docs/adapter-guide.md):
// my-stats.mjs
export const version = 2; // 必填:契约版本(固定 2)
export const name = "my-stats"; // 必填:唯一名(^[A-Za-z0-9_-]{2,64}$)
export const label = "我的统计"; // 可选:展示名
export const providers = ["my-relay"]; // 必填:认领的 provider 列表
/** 必填:获取原始数据(宿主端执行;入参由插件注入) */
export async function fetchData({ apiEndpoint, staticPath, apiKey, signal, timeoutMs }) {
const res = await fetch(apiEndpoint + staticPath, {
headers: { Authorization: `Bearer ${apiKey}` },
signal,
});
if (!res.ok) throw new Error(`http-${res.status}`);
return res.json(); // 只返回展示所需的最小数据集
}
/** 必填:胶囊内容(宿主端执行,返回 HTML 字符串) */
export function formatCapsule({ data, status, esc }) {
return `<span style="font-weight:600">${esc(data.visits ?? 0)} 次</span>`;
}
/** 必填:面板内容(宿主端执行,返回 HTML 字符串)
* 入参还注入共享图表工具 `utils`(可选):const U = utils || {} 后可直接
* 调 U.miniAreaSvg({...}) 画 SVG 迷你图(见 docs/adapter-guide.md §3.3) */
export function formatPanel({ entries, range, truncated, esc, utils }) {
const U = utils || {};
const rows = entries.slice(-60).map((e) =>
`<tr><td>${esc(new Date(e.time).toLocaleString("zh-CN"))}</td><td>${esc(e.data.visits)}</td></tr>`).join("");
return `<table>${rows}</table>`;
}
接线配置(推荐设置页承载,无需手改配置文件):
- 打开 dsh 设置 → 插件 →「用量统计」
- 在目标 provider 下点「+ 添加适配器」,输入 mjs 文件路径(支持
~展开 / 绝对路径) - 点「检测文件」回显导出信息 → 确认添加(自动持久化 + 热注册为该 provider 启用者)
- 切换启用 / 停用:候选行开关实时生效并持久化
也兼容 cordis.patch.yml 声明(可选,配置态叠加):
plugins:
'@wingsky-1/dsh-provider-usage':
adapter: ~/dsh/my-stats.mjs
provider: my-relay
staticPath: /api/usage
# autoReload 默认开启;如需关闭(安全/稳定性顾虑)显式声明:
# autoReload: false
加载失败 fail-fast 拒收并登记错误(设置面板可见),不影响插件其余功能。路径安全:相对路径只允许落在 DSH_HOME 或插件 home 内,未规整形态(../ 穿越)一律 400 拒绝。
v1 旧契约已删除
v1 旧契约已随破坏性变更 #932 删除,仅支持 v2 契约(fetchData 返回原始对象、
formatCapsule/formatPanel 返回 HTML 由宿主端渲染,name 字段白名单校验,
见 docs/adapter-guide.md)。设置页「用量统计」承载检测/添加/切换/停用
(自动持久化);cordis.patch.yml 声明仅为可选叠加。
安全模型
- 适配器代码 = 宿主完整 Node 权限(网络/文件/环境变量),等同用户自己写的进程内插件; 仅加载你信任的本地文件,插件绝不从网络拉取执行代码
- 密钥不进浏览器端:apiKey 仅存于宿主进程内存,经入参注入 fetchData; 适配器文件即使被静态服务暴露也不含密钥值(DeepSeek 官方内置适配器同样成立: 三级密钥链见上文,stats/history/adapters 响应体与胶囊/面板 HTML 均无密钥子串)
- XSS 双层防护:外部 API 数据流入 HTML 前必须经
esc()助手转义(文档义务); 插件在宿主端对所有 format 输出做结构化净化兜底(script/iframe/on* 属性/javascript: 协议移除), 且兜底净化封闭 HTML 实体编码变体——具名 / 十进制 / 十六进制、有无分号均解出后匹配, 协议型载体再按 WHATWG URL 语义剥除 Tab/LF/CR 后定位(jav ascript: 族同封); 净化只删不改并在宽松轮数上限内迭代收敛,超限 fail-closed 丢弃输出(约束最坏 CPU 成本): 解码副本仅用于定位、绝不回写输出,合法转义文本零损伤 - 热更新安全:默认开启(
autoReload,可显式关闭);开启后以 mtime+size 轮询检测变化,新版校验通过才原子切换, 失败保留旧版并登记错误 - 超时纪律(两层,勿混淆):
- 服务端取数跳:fetchData 强制 5s 超时(固定值,不可配置);宿主超时会主动 abort 真实 fetch——
下发给 fetchData 入参的
signal是合并信号(超时兜底 × 外部取消经手动级联监听合成, node>=20 全系兼容),适配器应把它透传给底层 fetch 的RequestInit.signal, 超时/取消时真正中断请求、不悬挂 socket;不透传时超时仅放弃等待,请求可能仍在后台完成。 - 客户端到 dsh web 跳(#268):客户端所有 HTTP 请求经
fetchTimeout封装, 默认 10sAbortSignal.timeout兜底(与 dsh-mcp-manager api() 的 #111 先例对齐)—— 移动端切后台形成半开连接时,死连接上的请求可能挂到 TCP 重传超时(可达 15 分钟), 该兜底保证浏览器侧有界等待、页面不悬挂;10s 大于服务端取数上限(5s),正常链路不误杀。 调用方自带signal时不启用兜底(避免双取消竞争)。 0 参声明的 fetchData 不读入参,完全兼容;取数锁为 per-provider 粒度,同 provider 并发请求 排队并复用首次取数结果——任何情况下不阻塞页面其他请求
- 服务端取数跳:fetchData 强制 5s 超时(固定值,不可配置);宿主超时会主动 abort 真实 fetch——
下发给 fetchData 入参的
- 报告生成(#503 M3;#532 年报化;#633 目录维度):零独立凭据、零新增网络出口——模型调用经宿主 llm 服务
(
ctx.llm.stream),凭据由 dsh 既有 provider 配置持有,插件不接触;生成不产生 session 事件、不入用量统计(消耗由报告元数据单独记录);报告配置/产物/lastRun 落盘historyRoot/reports/(0600);产物正文经 escape-then-transform 管线 (先转义、后引入无属性 h3/strong/ul/li/p 白名单标签,第一层)+sanitizeHtml(第二层)双层净化后方可入 tab,统计 JSON 注入面只含聚合数值与目录 basename (剥控制字符 + 截断 80,不含会话明细与完整路径;provider/model 名进快照前剥 控制字符并截断;目录名进快照前 basename 化——出口无路径分隔符);三周期各自 独立提示词模板(prompts{daily,weekly,monthly},旧单一模板读取时自动迁移; #633 起模板含目录观察,目录名一律 basename、只报数字不解读目录内容);空窗口 (无任何用量)不调模型;可选 notifier 推送默认关闭,摘要不含项目路径(仅周期、 窗口与总量/调用数数值); 手动生成异步任务化(#625):POST 立即返回 202+taskId,客户端轮询状态,与 LLM 耗时解耦(不再受 10s fetch 超时影响);默认幂等——窗口已有成功报告则复用(#626), 勾选「重新生成」强制覆盖。每个报告 attempt 的脱敏日志只含 attempt、结果/稳定 code、 opaque effort ID、input/output/reasoning/total/cache token 数字与 durationMs;绝不记录 prompt、reasoning 原文、API key/凭据或 path。reasoning 只作为数值 token 观测; 报告历史按窗口读侧投影去重(一行/窗口=最新版,index.jsonl 保持 append-only);lastRun 由 index 事实推导校准(schema v2,#624:旧语义「当天」 窗口自动识别为未闭环并回退,周一 06:00 不再吞日报) - fail-fast 加载:适配器缺导出/类型错/name 不合白名单 → 拒绝加载并登记可排障错误
- 历史数据:按天分片 JSONL 落盘(
0600权限),超龄/超量自动清理; 原始 data 在落盘前经过序列化校验(不可序列化对象拒收) - 所有路由 loopback 围栏(非回环 403 / 方法错 405);管理端点信息面最小披露 (用户文件只显示 basename);请求频率受控(轮询 ≤1 次/60s + 预热 ≤1 次/5min + 60s TTL 缓存)
验证
测试单份维护、变异自动覆盖:单元测试只维护 test/*.test.ts(import "../lib/index.js" 测产物);stryker 经 lib→src hook 复用同一份断言,无需手工同步副本。
# 健康检查(回环)
curl -s http://127.0.0.1:3080/api/dsh-provider-usage/health
# 源码在 src/,改后必须 build
pnpm --filter @wingsky-1/dsh-provider-usage build
pnpm --filter @wingsky-1/dsh-provider-usage test
License
MIT
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。