dsh-plugin-usage-analytics
一款给 DeepSeek Harness(DSH)用的本地用量分析插件:订阅会话事件,把工具调用、回合、错误、耗时与能力使用情况聚合成日表,在侧栏面板里展示,并对外提供一套本地 HTTP 查询接口。默认 local 模式、零出站,不采集任何消息正文。
配图说明:上图是本插件随包发布的
icon.svg(32×32,侧栏与插件列表共用)。面板截图需要在宿主加载插件后由本机浏览器截取,见docs/design-mapping.md的「验证状态」一节。
特性
- 零依赖:纯 ESM JavaScript,只
importnode:内置模块(node:crypto),不需要 TS/打包工具链,装上即可跑。 - 三层幂等:游标(
meta.cursors[sid])+ fork 起点(inheritedEventCount)+ 回填去重,重启、重载、fork 都不会重复计数。 - 脏表才写:内存聚合 → 每
flushIntervalMs(默认 10s)只把脏表落盘,写放大可控。 - 可降级:取不到
storageDomain时自动切内存档,面板与接口照常工作,只是重启即丢(diagnostics.storage.kind === 'memory')。 - 可关:
collect:false停止采集,selfExclude排除本插件的工具调用,retentionDays控制保留天数(同时裁剪内存与落盘)。 - 围栏与限流:
Host必须是回环名(127.0.0.1/localhost/[::1],挡 DNS rebinding)+Sec-Fetch-Site+Origin/Host一致性校验,写方法面每秒 10 次上限,请求体 64KB 上限。
安装
正路(推荐):
dsh plugin --profile <profile> add github:wjf1/dsh-plugin-usage-analytics
无需构建:package.json 声明了 dsh.bundle(patch: ./cordis.patch.yml)与 dsh.client(web UI),lib/ 产物随仓库提交。
离线安装 / 手工等价(本机当前就是这种装法,等价于上面三步):
- 把包目录放进 profile 的
node_modules(Junction 或直接拷贝):C:\Users\admin\.dsh\profiles\desktop\node_modules\dsh-plugin-usage-analytics - 在 profile 的
package.json的dsh.profile.bundles数组里追加"dsh-plugin-usage-analytics"(不要加进dependencies,bundle 不需要 npm 解析)。 - 重启 DSH(新增 bundle 可以靠 HMR 加载,替换已装包必须重启;本仓库采用 Junction,改动即时可见但首次加载仍需宿主扫到 bundle 列表)。
装好后打开侧栏「用量分析」即可看到面板;GET /dsh-plugin-usage-analytics/api/ 应返回 {"ok":true,"value":{"ready":true,...}}。
配置
写在 cordis.patch.yml 的 config 段(也可在运行时通过设置接口热改,仅白名单键):
| 键 | 默认 | 说明 |
|---|---|---|
mode |
local |
local 只落本地;remote/opt-in 为未来上报预留,当前不产生任何出站流量 |
collect |
true |
总开关,false 时不再消费会话事件 |
sampleRate |
1 |
采样率(0..1),按 tool/call+tool/result 成对抽稀,丢弃量记在 meta.sampled |
retentionDays |
90 |
保留天数(1..400),定时任务按此裁剪 |
maxEventsPerDay |
200000 |
单日事件上限,超出记 meta.dropped |
flushIntervalMs |
10000 |
合并落盘间隔,最小 2000 |
layout |
single |
存储布局,single 整单元重写 / per-record 逐条写(小数据量更稳,大数据量注意小文件数量) |
selfExclude |
false |
是否排除本插件自身产生的调用 |
profileName |
null |
存储 domain 的 profile 后缀。默认自动探测(config → profile 服务 → app.profile → DSH_PROFILE/DSH_PROFILE_NAME/DSH_PROFILE_DIR → --profile= → ctx.baseDir 的 profiles/<name> → default),显式指定可保证多 profile 隔离 |
验证状态(真机)
| 检查项 | 结果 | 证据 |
|---|---|---|
| 宿主加载 bundle | ✅ | diagnostics.version 有值、routes 注册成功 |
| HTTP 前缀可达 | ✅ | GET /dsh-plugin-usage-analytics/api/ → ready:true(签名 cookie 鉴权后) |
| 真实数据聚合 | ✅ | 1181 次调用 / 22 次错误 / 4 会话 / tokensIn 3,354,030 / avgMs 3505 |
| 存储落盘 | ✅ | storage.kind=domain、writes=158、flushes=32、C:\Users\admin\.dsh\storages\plugin_analytics_default.json 14KB |
| JSON-RPC 面 | ✅ | POST / 的 diagnostics 返回统一信封 |
| 客户端包注册 | ✅ | GUI boot 清单含 dsh-plugin-usage-analytics/client.js |
| 面板肉眼验收 | ⚠️ | 需重启宿主后在 GUI 侧栏确认(会话内无浏览器控制权) |
面板
侧栏入口「用量分析」,主面板包含:
- 6 张 KPI 卡:窗口内工具调用、错误数、错误率、平均耗时、会话数、tokens 进出;
- 14 日趋势柱状图(SVG 自绘,不引第三方图表库);
- Top 工具表(调用数、错误数、平均耗时、占比);
- 能力分布(MCP / skill 等非原生能力的调用占比条);
- 设置区:
collect勾选、sampleRate滑杆、retentionDays数字框; - 操作区:刷新、立即落盘、清空本地数据(二次确认)、窗口天数切换(1/7/14/30/90)。
配色只用宿主 --dsw-alias-* 主题 token(带回退值),跟随浅色/深色主题。
HTTP 接口
前缀 API_PREFIX = /dsh-plugin-usage-analytics/api。
JSON-RPC 方法面(设计 §7.5)
POST <prefix>/,请求体 { "method": "...", "params": { ... } },响应 {ok:true,value} 或 {ok:false,error:{code,message,retryable?,detail?}}。
| method | params | 说明 |
|---|---|---|
overview |
{range} |
4 KPI + 窗口汇总 + Top 工具 |
plugins |
{range,q?,sort?,cursor?} |
工具/能力榜,每页 50 |
plugin |
{pluginId,range} |
单工具逐日明细 |
capabilities |
{range,type?,top?} |
能力排行(mcp/skill) |
env |
{range} |
宿主与插件版本、存储状态 |
series |
{range,metric} |
逐日序列(calls/errors/sessions/turns) |
events |
{limit?} |
最近会话摘要(聚合粒度,见偏差说明) |
settings.get / settings.set |
{} / {patch} |
读/改设置(白名单) |
consent.get / consent.set |
{} / {mode,version} |
同意态(只支持 local/off) |
report.preview / report.now / report.clear |
{} |
上报预览/立即/清空(未启用上报,返回空载荷) |
data.delete |
{confirm:'DELETE'} |
删除本地全部数据 |
export |
{range,format:'json'|'csv'|'ndjson'} |
内联导出 |
diagnostics |
{} |
运行态诊断 |
reset |
{} |
清空并重扫 |
range 接受数字、"7d"、{days}、{from,to},上限 366 天;错误码 E_ORIGIN/E_RATE/E_RANGE/E_PAYLOAD/E_CONSENT/E_METHOD/E_INTERNAL 映射到设计 §7.5 的状态码表。
REST 便捷端点(面板使用)
GET /、/overview?days=、/series?days=、/sessions?limit=、/tools?days=、/tool?name=&days=、/settings、/diagnostics、/export(NDJSON 下载);POST /settings、/flush、/reset?confirm=RESET。与 JSON-RPC 共用同一批聚合函数。
分页上限统一由
lib/shared.js的PAGE_SIZE/MAX_PAGE_SIZE定义(REST 与 JSON-RPC 共用),/sessions默认 20 条。
POST /reset与 RPC 的reset都必须显式带确认(?confirm=RESET/params.confirm:"RESET"),否则回E_CONFIRM。
存储
每个 profile 一个 domain:plugin_analytics_<profile 名>(storageDomain.open),默认 layout: 'single',落盘位置由宿主的 DSH_HOME/storages 决定。表:
meta:installId、settings、cursors、dropped、sampleddaily:按天的总量(toolCalls/toolErrors/turns/sessions/tokens/ms)toolDaily:day + tool维度capDaily:day + 能力键维度(MCP / skill)sessions:会话首见与累计(turns/toolCalls/errors/model)
隐私
- 只记录事件元数据:工具名、能力分类、成败、耗时、会话短哈希、模型名、回合/步数、token 计数。
- 不记录:消息正文、工具参数原文、工具输出、文件路径、用户名、环境变量。
- 会话 ID 以
sha256(sessionId + ':' + installId)截 16 位存储,卸载重装即换新installId,历史无法互相拼接。 - 默认
local模式,代码里没有任何出站请求(可grep -R "http" lib/自证:只有http://localhost的 URL 解析基线)。
开发
无构建步骤,直接改 lib/*.js:
node --test test/store.test.mjs test/aggregate.test.mjs test/collector.test.mjs \
test/routes.test.mjs test/rpc.test.mjs test/index.test.mjs
75 条测试全通过。node --test test/(只给目录)在 Windows 上会报 Cannot find module ...\test,必须显式列文件。
已知限制
见 docs/design-mapping.md:设计里的 plugins/env/events/report/SDK/上报通道在本实现中是明确降级(数据模型不含插件清单与逐事件行),逐条列了原因与替代方案。设计 v1.0 与评审报告不在本仓库内。
许可证
MIT。
dsh-plugin-usage-analytics (English)
A local-only usage analytics plugin for DeepSeek Harness. It subscribes to session events, aggregates tool calls, turns, errors, latencies and capability usage into daily rows, renders them in a sidebar panel, and exposes a fenced local HTTP API.
Highlights
- Zero runtime dependencies: plain ESM JavaScript, only
node:builtins; no build step. - Idempotent ingestion: per-session cursor +
inheritedEventCountfork boundary + backfill dedupe. - Write amplification control: in-memory aggregation, dirty tables flushed every
flushIntervalMs(default 10s). - Graceful degradation: falls back to an in-memory store when
storageDomainis unavailable (diagnostics.storage.kind === 'memory'). - Switches:
collect,selfExclude,sampleRate,retentionDays. - Fence + rate limit: the
Hostheader must be a loopback name (127.0.0.1/localhost/[::1]— this is what stops DNS rebinding), plusSec-Fetch-Site/Originchecks, 10 writes per second, 64KB body cap.
Image note:
icon.svg(32×32) ships with the package and is used by both the sidebar and the plugin list. Panel screenshots must be taken in a local browser after the host has loaded the plugin; see the "validation status" section ofdocs/design-mapping.md.
Install
Preferred: dsh plugin --profile <profile> add github:wjf1/dsh-plugin-usage-analytics — package.json declares a dsh.bundle manifest (plus dsh.client for the web UI), so it installs with no build step.
Offline / manual equivalent:
- Put the package directory into the profile's
node_modules(junction or plain copy):~/.dsh/profiles/<profile>/node_modules/dsh-plugin-usage-analytics - Append
"dsh-plugin-usage-analytics"todsh.profile.bundlesin the profile'spackage.json(not todependencies— a bundle needs no npm resolution). - Restart DSH. A newly added bundle can activate through HMR; replacing an installed package requires a restart.
Once loaded, open "Usage Analytics" in the sidebar; GET /dsh-plugin-usage-analytics/api/ should return {"ok":true,"value":{"ready":true,...}}.
Configuration
Keys are written under config in cordis.patch.yml (whitelisted keys are also hot-editable at runtime via the settings method):
| Key | Default | Meaning |
|---|---|---|
mode |
local |
local keeps everything on disk; remote/opt-in are reserved for future reporting and produce no outbound traffic today |
collect |
true |
Master switch; false stops consuming session events |
sampleRate |
1 |
Sampling rate (0..1), thins tool/call+tool/result pairs; drops are counted in meta.sampled |
retentionDays |
90 |
Retention window (1..400), pruned by a scheduled job |
maxEventsPerDay |
200000 |
Per-day event cap; overflow is counted in meta.dropped |
flushIntervalMs |
10000 |
Dirty-table flush interval, minimum 2000 |
layout |
single |
Storage layout: single rewrites the whole unit / per-record writes row by row (safer for small datasets, watch the file count when data grows) |
selfExclude |
false |
Exclude calls made by this plugin itself |
profileName |
null |
Profile suffix for the storage domain; auto-detected by default (config → profile service → app.profile → DSH_PROFILE/DSH_PROFILE_NAME/DSH_PROFILE_DIR → --profile= → profiles/<name> under ctx.baseDir → default). Set explicitly to guarantee isolation between profiles |
API
POST /dsh-plugin-usage-analytics/api/ with {method, params} — 18 design methods: overview, plugins, plugin, capabilities, env, series, events, settings.get/set, consent.get/set, report.preview/now/clear, data.delete, export, diagnostics, reset. Convenience REST GETs back the sidebar panel. Requests are gated by a loopback Host (blocks DNS rebinding) plus Sec-Fetch-Site and Origin/Host consistency, capped at 10 writes per second and 64KB per body. Both the REST /reset?confirm=RESET endpoint and the RPC reset method require an explicit confirmation.
Validation status (real host)
| Check | Result | Evidence |
|---|---|---|
| Bundle loaded by host | ✅ | diagnostics.version populated, routes registered |
| HTTP prefix reachable | ✅ | GET /dsh-plugin-usage-analytics/api/ → ready:true (with signed cookie) |
| Real data aggregated | ✅ | 1181 calls / 22 errors / 4 sessions / tokensIn 3,354,030 / avgMs 3505 |
| Storage persisted | ✅ | storage.kind=domain, writes=158, flushes=32, a 14KB storage unit on disk |
| JSON-RPC surface | ✅ | POST / diagnostics returns the standard envelope |
| Client half registered | ✅ | GUI boot manifest lists dsh-plugin-usage-analytics/client.js |
| Panel eyeball check | ⚠️ | Needs a host restart, then a look at the GUI sidebar |
Development
No build step — edit lib/*.js directly and run:
node --test test/store.test.mjs test/aggregate.test.mjs test/collector.test.mjs \
test/routes.test.mjs test/rpc.test.mjs test/index.test.mjs
75 tests, all passing. Passing only the test/ directory makes node --test fail on Windows (Cannot find module ...\test) — list the files explicitly.
Known limitations
See docs/design-mapping.md: the plugins/env/events/report methods, the SDK and the reporting channel are deliberately degraded in this implementation (the data model keeps no plugin list and no per-event rows); each case is listed with its reason and the substitute used instead. Design v1.0 and its review report live outside this repository.
Privacy
Metadata only: tool name, capability class, ok/error, latency, short session hash, model name, turn/step counts, token counters. No message bodies, arguments, outputs, paths, or usernames. Local mode performs zero outbound requests.
License
MIT.
No comments yet. Be the first to write one.