EIGHTfs/dsh-skill-scoreboard ↗★ 0

dsh-skill-scoreboard

DSH skill 使用记分板:同时记录按会话去重与每次加载两种次数;设置页三选项卡(Skill 排行 / 会话榜 / 管理),Skill 排行固定按会话去重降序并分页(两种次数同时显示),会话榜按去重 skill 数排列并显示持久化会话标题;skill 工具与 read 直接读 skill 文件均记分;agent/pre-step 注入记分榜与 skill 实际路径;导入导出记分 JSON。 适合需要统计、分析和管理AI Skill加载历史与会话排行数据的用户。

パッケージ
dsh-skill-scoreboard
互換性
未検証
Harness ピア範囲
>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0
バージョン
1.0.4
最終更新
2026/09/19

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:EIGHTfs/dsh-skill-scoreboard

ドキュメント

README 全文を読む ↗

dsh-skill-scoreboard

skill 使用记分板:模型每真正加载一个 skill 就自动记分,无需手动维护 skill-scoreboard.md。运行时数据写在 $DSH_HOME/.dsh/skill-scoreboard/skill-usage.json,默认 git 忽略。v1.3.0 在设置侧边栏提供「Skill 记分板」页面;v1.4.0 起在 agent/pre-step 把记分榜与 skill 实际路径注入给 AI;v1.6.0 同时记录两种记分规则(按会话去重 / 每次加载);v1.8.0 页面改为三选项卡(Skill 排行 / 会话榜 / 管理)并新增会话维度排行榜;v1.8.1 Skill 排行固定按会话去重降序(去重与加载两种次数同列显示,不再切换规则);v1.8.2 会话榜显示持久化会话标题(历史会话也能解析出标题,取不到则显示短 id),记分扩展为 read 工具直接读 skill 文件同样记分;v1.8.3 代码质量重构(拆分长函数/降圈复杂度/提取重复字面量,行为不变)。

界面预览

Skill 排行(按会话去重降序,去重与加载两种次数同列显示,分页 20 条/页):

Skill 排行

会话榜(按去重 skill 数降序,可展开看该会话加载过哪些 skill,可点击打开会话):

会话榜

管理(导出 / 导入(合并、覆盖)/ 数据概览):

管理

界面模拟页:assets/preview.html(单文件自包含,双击即可打开;跑的是真实 lib/client.js + 假数据, 三选项卡、分页、会话展开、导入导出全部可点,改动只留在页面内) 截图声明:screenshots.json(仓库根、与 package.json 同级,列出 assets/panel-*.png)—— DSH 插件榜单按此文件自动收录截图,作者推自己的仓库即生效,无需到榜单仓库提 PR

目录

架构设计

核心链路:监听 tools/result → skill 工具执行成功后记分 → 落盘 → 宿主只读 API → 设置页展示;另在 agent/pre-step 注入记分榜给 AI。

  • 判定:exec.name === "skill",从 arguments.name 取 skill 名;结果带 isError 的不计分。
  • 两种次数:count 按会话去重(同会话同一 skill 只计 1,跨会话累加);loads 每次成功加载都 +1;callId 防同一调用重复写。
  • 会话表(v1.8.0):记分时同步写顶层 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 独立页面,三个选项卡)。
  • 注入(v1.4.0+):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.ymlbundle 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 发布元数据
.npmignorenpm pack 排除规则:*.bak / .trash/ / 锁文件(npm pack --dry-run 验证 6 文件入库)

启动脚本

本插件随 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 --import ./test/_hooks/host-resolver.mjs test/test-client.mjs       # 前端渲染冒烟

# 6) 改 patch / 插件代码(宿主半侧)后重启 web profile 才会加载新 hооk;
#    只改 lib/client.js 可用 clientModules.rebuilt('dsh-skill-scoreboard') 热刷新(免重启)

启动成功时进程日志:

[skill-scoreboard] ✅ 已启动,数据文件: …/.dsh/skill-scoreboard/skill-usage.json,当前记录 N 个 skill
[skill-scoreboard] ✅ 已注册 GET /api/skill-scoreboard + export/import

API 总览

记分在 tools/result 上自动发生;v1.2.0 起提供只读查询 API 供设置页使用;v1.8.0 起返回体带会话榜。

入口说明
tools/result观察 skill 工具最终结果记分;v1.8.2 起 read 工具直接读 skill 文件路径同样记分(同步更新 skill 榜与会话表)
GET /api/skill-scoreboard只读:返回 {ok, total, totalLoads, recorded, updatedAt, dataFile, skills:[…], sessions:[…]}。skills 按 count 降序(最多 200 条),sessions 按 distinct 降序(并列按 loads、再按 lastUsedAt,最多 200 条);v1.8.2 起每行带 title(宿主解析的持久化会话标题,兜底短 id)
GET /api/skill-scoreboard/export导出完整记分 JSON(version:2 / skills / sessions / updatedAt / exportedAt)
POST /api/skill-scoreboard/import?merge=true|false导入记分 JSON;默认 merge 累加合并(含会话表),merge=false 整表替换。v1 旧格式(无 sessions)自动反推会话表
配置 enabledfalse 时不挂监听
配置 dataFile覆盖默认数据路径(默认 $DSH_HOME/.dsh/skill-scoreboard/skill-usage.json)
配置 injectEnabled(v1.4.0)false 时不注入记分榜(默认 true)
配置 injectTopN(v1.4.0)注入 Top N 个 skill 及路径(默认 25)

数据结构

v1.8.0 起为 v2:在 v1 的 skills 之外新增顶层 sessions 会话表。

{
  "version": 2,
  "skills": {
    "dsh-restart-gate": {
      "count": 2,
      "loads": 3,
      "lastUsedAt": "2026-09-10T08:57:00.000Z",
      "sessions": ["session-"],
      "callIds": ["call-0296d410-70b8-4e49-896a-2ce906ea8993-174"]
    }
  },
  "sessions": {
    "session-": {
      "loads": 3,
      "distinct": 2,
      "skills": { "dsh-restart-gate": 2, "full-context-read": 1 },
      "firstUsedAt": "2026-09-10T08:50:00.000Z",
      "lastUsedAt": "2026-09-10T08:57:00.000Z"
    }
  },
  "updatedAt": "2026-09-10T08:57:00.000Z"
}
  • skills[name].count:按会话去重的累计次数(跨会话累加)
  • skills[name].loads:每次成功加载都 +1 的累计次数
  • skills[name].sessions:已计分的会话 id 列表(用于去重判定)
  • skills[name].callIds:已计分的工具调用 id(防同一调用重复写)
  • sessions[id].loads:该会话加载 skill 的总次数
  • sessions[id].distinct:该会话加载过的不同 skill 数(会话榜排序主键,越多越靠前)
  • sessions[id].skills:该会话每个 skill 的加载次数(会话行展开时展示)
  • sessions[id].loadsEstimated:v1 迁移估计标记(旧数据无法还原每次加载,loads 用 distinct 兜底)

v1 → v2 迁移:读取旧文件时自动迁移(v1 只有 skills[].sessions[],反推出会话表;distinct 准确、loads 为估计值并标记 loadsEstimated),下次写盘即为 v2。

注意:数据文件默认路径为 $DSH_HOME/.dsh/skill-scoreboard/skill-usage.json(非插件源码目录下的 data/skill-usage.json)。可通过配置项 dataFile 覆盖路径。

查排行:

# 默认路径
python3 -  {`、末尾 `})();`),顶层声明归零,与官方 `factory` 闭包效果一致,**零改名**。**验证**:`vm` 作用域实测 11 个关键声明**零泄漏到全局**;与 `dsh-session-migrate` 拼接后 `node --check` 与 `new Function` 双双通过(反证:去掉对方 IIFE 即复现 `SyntaxError: Identifier 'SETTINGS_SECTION_ORDER' has already been declared`);真实 React 环境下模块注册 / factory / apply / 组件渲染四步全过;30 断言全过 |
| 1.0.3 | **1/4** **修复「设置里有入口、点进去空白」**:`dictListeners` / `subscribeDict` / `loadDict` 三个外置 i18n 助手此前被定义在 `createModule` factory **内部**(2026-09-14 字典外置化时引入),而消费它们的 `ScoreboardPage` 是**顶层函数**——组件在 `useEffect` 里调用 `subscribeDict` 抛 `ReferenceError: subscribeDict is not defined`,React 随即卸载整棵树,表现为设置面板点进去一片空白(宿主日志与接口均无异常,属静默失败)。已把三者移回顶层作用域(与 `L` / `NS` 同级),30 断言仍全过。(该缺陷同时促使 preview 预览页改用真实宿主渲染语义,见 1.0.4 ③)
**2/4** **字典外置化(方案A,替代 Loader 内嵌修复)**:弃用「内嵌 JS 对象字面量」,改为宿主半侧把外置 `lib/i18n/{zh,en}.json` 经新增 `GET /api/skill-scoreboard/i18n` 暴露、客户端 `apply` 后由 `loadDict()` fetch 拉取合并进 `L`——DSH 浏览器端 `require` 不支持相对路径 JSON 的解法即绕开 require 走 HTTP。源码仅保留导航标签 `settings.title/desc` 最小同步兜底,语言仍跟随 DSH 全局界面语言;`lib/i18n/*.json` 成为单份权威,改 dict 无需重打包 client bundle。`test-client.mjs` 假 fetch 按 URL 分流(i18n→真实外置 json,其余→快照)适配新架构,30 断言全过
**3/4** **Loader 修复**:客户端字典 `require('./i18n/*.json')` 在 DSH 浏览器端 ModuleLoader 下抛 `client-modules: require(...) missed the module table`(宿主 require 只认模块表词/seed/已注册工厂,不解析相对路径 JSON)——字典内嵌为 JS 对象字面量(源文本保留 `lib/i18n/*.json`),插件不再加载失败
**4/4** **打包修复 + 补 peer 声明**:`lib/` 下三份备份文件(`client.js.bak` / `index.js.bak` / `routes.js.bak`,共 78KB)虽未被 git 跟踪,但 `files` 白名单是目录级的(`lib`),`npm pack` 会把它们一并打进发布包 —— 已移入回收站,发布包文件数 16 → 13;新增 `peerDependencies` 声明宿主提供的 `@deepseek-ai/dsh-llm` 与 `@deepseek-ai/schemastery`,范围写成带显式预发布分支的形式(`>=0.0.1-rc.1 =0.1.0-rc.1 ` 重复插入一份 ``;改用顶层常量 `NS` 后只注入一次。`test-client.mjs` 补该 DOM 注入路径的回归断言;**1.8.2** **会话标题 + read 直接读文件记分**:会话榜此前全部显示「无标题会话」——根因是 client 侧 `sessions.list` 只含活跃会话、历史会话查不到标题。改为宿主半侧解析标题注入 API:①活跃会话快照 `displayTitle` → ②`sessionPersistence.inspect` 读日志折叠 `session/title` 事件(与 dsh-session-conductor 同法)→ ③`cwd` 目录名 → ④短 id 兜底;**性能修复**:标题解析限并发(同时最多 2 个 inspect)