dsh-skill-scoreboard
Skill usage scoreboard for DSH: counts both per-session deduplicated and per-load usage, scores `skill` tool calls and direct `read` of skill files, and shows the tally in a three-tab settings page (skill ranking, session board, management) with paging and JSON import/export.
Install
dsh plugin --profile web add dsh-skill-scoreboard
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-skill-scoreboard
skill 使用记分板:模型每真正加载一个 skill 就自动记分,无需手动维护skill-scoreboard.md。运行时数据写在$DSH_HOME/.dsh/skill-scoreboard/skill-usage.json,默认 git 忽略。
功能要点:设置侧边栏独立页面「Skill 记分板」(三选项卡:Skill 排行 / 会话榜 / 管理;Skill 排行固定按会话去重降序,去重与加载两种次数同列显示并分页;会话榜显示持久化会话标题,可展开、可打开会话);agent/pre-step 把记分榜与每个 skill 的实际文件路径注入给 AI;同时记录两种次数(按会话去重 / 每次加载);read 工具直接读 skill 文件同样记分;管理页可导出 / 导入记分 JSON。
界面预览
Skill 排行(按会话去重降序,去重与加载两种次数同列显示,分页 20 条/页):
会话榜(按去重 skill 数降序,可展开看该会话加载过哪些 skill,可点击打开会话):
管理(导出 / 导入(合并、覆盖)/ 数据概览):
界面模拟页:assets/preview.html(单文件自包含,双击即可打开;跑的是真实lib/client.js+ 假数据,
三选项卡、分页、会话展开、导入导出全部可点,改动只留在页面内)
截图声明:screenshots.json(仓库根、与package.json同级,列出assets/panel-*.png)——
DSH 插件榜单按此文件自动收录截图,作者推自己的仓库即生效,无需到榜单仓库提 PR
目录
- [界面预览](#界面预览)
- [架构设计](#架构设计)
- [文件目录结构及作用](#文件目录结构及作用)
- [启动脚本](#启动脚本)
- [API 总览](#api-总览)
- [数据结构](#数据结构)
- [页面结构(三选项卡)](#页面结构三选项卡)
- [版本列表](#版本列表)
- [注意事项](#注意事项)
- [开发计划 / 疑难杂症](#开发计划--疑难杂症)
架构设计
核心链路:监听 tools/result → skill 工具执行成功后记分 → 落盘 → 宿主只读 API → 设置页展示;另在 agent/pre-step 注入记分榜给 AI。
- 判定:
exec.name === "skill",从arguments.name取 skill 名;结果带isError的不计分。 - 两种次数:
count按会话去重(同会话同一 skill 只计 1,跨会话累加);loads每次成功加载都 +1;callId防同一调用重复写。 - 会话表:记分时同步写顶层
sessions表({ [sessionId]: { loads, distinct, skills, firstUsedAt, lastUsedAt } }),记录该会话加载过哪些 skill、各几次,供会话榜使用。 - 路径解析:注入时经
skills服务(get/list的path/resourceBase)解析 skill 实际文件路径,失败则扫描ai-work-archive/skills/、插件skills/、.dsh/skills/等目录兜底;设置页 UI 不显示路径。 - 展示:宿主端
GET /api/skill-scoreboard只读接口(返回 skill 榜 + 会话榜 + 概览字段)+ 浏览器半侧挂 设置 → 侧边栏 →「Skill 记分板」(settings.section独立页面,三个选项卡)。 - 注入(+):
agent/pre-step事件(与 dsh-git-push 相同时机),每个 agent 首次 step 注入一次:记分榜 Top N(含会话去重 / 加载两种次数)+ 每个 skill 的实际文件路径。开关injectEnabled、条数injectTopN。 - 不扫会话日志:
Session没有公开events字段,且agent/pre-step发生在本步skill调用之前,扫日志会漏记——因此记分只在tools/result发生。
skill 工具执行成功
↓
tools/result 事件
↓
name === "skill" 且结果非错误
↓
skill-usage.json(skills 榜 + sessions 会话表)
←── GET /api/skill-scoreboard(宿主只读接口)
↓
设置侧边栏「Skill 记分板」三选项卡页面(浏览器)
↓
agent/pre-step(每个 agent 首次 step)
↓
记分榜 Top N + skill 实际路径 注入给 AI
文件目录结构及作用
路径 · 作用
lib/index.js · 插件入口:apply 监听 tools/result,同时记 count(会话去重)、loads(每次加载)与 sessions 会话表;注册 GET /api/skill-scoreboard(含会话榜)与 export/import;agent/pre-step 注入记分榜 + skill 实际路径
lib/store.js · 宿主半侧 · 数据层(1.0.2 从 index.js 拆出):读 saveData(临时文件 + rename 原子写)/ 归一化 normalizeScoreboardPayload / 合并 mergeScoreboard / 记分 recordSkillUse·recordSessionUse
lib/skillpath.js · 宿主半侧 · 路径解析(1.0.2 拆出):「skill 名 ↔ 实际文件路径」双向解析——resolveSkillPath(先问 skills 服务、失败扫文件系统兜底)与 makeSkillPathIndex(懒构建 + TTL 的路径索引,供 read 工具直接读 skill 文件记分)
lib/titles.js · 宿主半侧 · 会话标题(1.0.2 拆出):四级取标题(活跃会话快照 → 折叠日志 session/title 事件 → cwd 目录名 → 短 id 兜底),限并发 + 落盘缓存 session-titles.json + warm 预热
lib/routes.js · 宿主半侧 · HTTP 路由(1.0.1 拆出):GET /api/skill-scoreboard 与 export/import 端点
lib/util.js · 通用小工具(1.0.1 拆出):createSemaphore / shortIdOf / errMsg 等零依赖助手
lib/client.js · 浏览器半侧:注册 settings.section 侧边栏「Skill 记分板」+ 三选项卡页面(Skill 排行 / 会话榜 / 管理),排行分页、会话标题解析与打开会话、导入导出,中英双语
cordis.patch.yml · bundle patch:insert id: skill-scoreboard
skills/dsh-skill-scoreboard.md · 插件手册 skill
test/test-scoreboard.mjs · 宿主半侧单测:记分去重 + 会话表 + v1→v2 迁移 + API/导入导出(mock ctx + 临时数据文件)
test/test-client.mjs · 前端冒烟测试:最小 React 替身渲染三选项卡、翻页、分页、会话展开、管理页
test/_hooks/host-resolver.mjs · 测试期宿主依赖解析钩子(零依赖方案):把 @deepseek-ai/* 裸导入解析到宿主已装的 node_modules,使单测能 import lib/index.js——仓库不需装依赖、不需软链接
$DSH_HOME/.dsh/skill-scoreboard/skill-usage.json · 运行时记分数据(git 忽略,不入库)
package.json · 包名 / 版本 / dsh.bundle.patch / dsh.skills / dsh.client / files+repository 等 npm 发布元数据
.npmignore · npm pack 排除规则:*.bak / .trash/ / 锁文件(npm pack --dry-run 验证 6 文件入库)
安装
装到哪个 profile 由 --profile 决定。从 GitHub 源安装(公开仓库,匿名可装):
⚠️ dsh plugin --profile <名> add … 当前实测会挂死且零输出(两次都以 timeout 结束、日志为空)⇒
改在该 profile 目录里直跑 pnpm(dsh plugin 的设计就是把参数转发给 profile 的 pnpm,两者本应等价):
cd <DSH_HOME>/profiles/<profile> pnpm add github:EIGHTfs/dsh-skill-scoreboard#<commit> # 用 commit 固定版本(远端暂无 tag)
两条前置条件(缺一即失败;本插件有 prepare/prepack,两条都需要):
1. 本机要有 npm —— pnpm 构建 git 来源的包时会在临时目录执行 npm install;
缺 npm 时报 ERR_PNPM_PREPARE_PACKAGE + spawn ENOENT: npm: command not found。
2. pnpm 11 默认禁止 git 源依赖执行构建脚本(供应链策略)⇒ 先在 profile 的
pnpm-workspace.yaml 里放行,否则报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED:
allowBuilds:
dsh-skill-scoreboard@https://codeload.github.com/EIGHTfs/dsh-skill-scoreboard/tar.gz/<commit>: true
⚠️ key 里带 commit 哈希 ⇒ 换版本/换提交要同步改这一行。
装完三步:profile package.json 的 dsh.profile.bundles 登记插件名 → 本插件 cordis.patch.yml
顶层 - insert: 新建 loader 行 → 重启宿主并在日志确认 loaded。
本地源码安装(file: 依赖 + 手工三步曲)见下一节。
启动脚本
本插件随 DSH web profile 装载,没有独立进程,也没有 start.sh。
# 1) 源码进 profile(真实目录拷贝,不要软链到无 node_modules 的源码目录) cp -a ./dsh-skill-scoreboard profiles/web/local-plugins/dsh-skill-scoreboard # 2) package.json 声明 file: 依赖,并写入 dsh.profile.bundles # "dsh-skill-scoreboard": "file:./local-plugins/dsh-skill-scoreboard" # 3) cordis.patch.yml insert # - insert: # - id: skill-scoreboard # name: dsh-skill-scoreboard # config: # enabled: true # 4) dry-run:能 import,且导出 name / inject / apply cd profiles/web node --input-type=module -e 'import * as m from "dsh-skill-scoreboard"; console.log(m.name, m.inject, typeof m.apply)' # 5) 自测(零依赖:宿主依赖经 test/_hooks/ 解析钩子注入,仓库内不装 node_modules) node --import ./test/_hooks/host-resolver.mjs --test test/*.mjs # 两套单测一起跑 node --import ./test/_hooks/host-resolver.mjs test/test-scoreboard.mjs # 宿主半侧 node …(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::4,最后更新 2026-10-17