dsh-skills-manager
Verified@zfdx123/dsh-skills-manager · v1.0.9 · MIT · Web UI
DSH 技能管理器:诊断、新建、编辑、开关、搬运磁盘上的技能(SKILL.md),补上官方 provider 静默跳过坏文件的那半边,并给模型一个 skill_manager 工具
Install
dsh plugin add @zfdx123/dsh-skills-manager Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Readme
@zfdx123/dsh-skills-manager
给 DeepSeek Harness(DSH)用的技能管理器:把散落在磁盘上的技能(<root>/<name>/SKILL.md 目录包,或 <root>/<name>.md 单文件)当成可管理的对象——能诊断、能编辑、能开关、能搬运、能删除,并补上官方 provider 缺的那一半。它同时给模型一个 skill_manager 工具,让「给某类任务现场造一个技能」变成对话里的一句话。
安装
npm 安装(推荐):
dsh plugin --profile web add @zfdx123/dsh-skills-manager
想把整套插件(目前 7 个)一次装齐,用聚合包:
dsh plugin --profile web add @zfdx123/dsh-atelier
两者不要同时装:同一个插件 id 会被插进组合树两次。要单个就只装那一个。
本插件是一个 profile bundle(与 dsh-mcp-manager 同形态),装进哪个 profile 就在哪个 profile 生效;装完重启 dsh web(组合树在进程启动时装配)。
本地目录安装(改代码时用):
# 1. 把包加进 profile 的依赖
cd ~/.dsh/profiles/web
# 编辑 package.json 的 dependencies,加一行:
# "@zfdx123/dsh-skills-manager": "link:E:/work/ai/dsh-atelier/packages/dsh-skills-manager"
# 并把包名加进 dsh.profile.bundles 数组
# 2. 安装(建立符号链接)
pnpm install
# 3. 重启 dsh(组合树在进程启动时装配)
验证安装
重启后:
- 设置页应出现 技能管理;
- 侧栏左下应出现 技能;
- 让模型调
skill_manager(action: list)应能列出技能。
如果设置页没出现,先看宿主日志里有没有 loaded without registering "<id>" via __ModuleLoader__.load——那是客户端注册 id 与包名不一致(客户端 bundle 的注册 id 必须等于包名 @zfdx123/dsh-skills-manager,写成短名会在挂载阶段直接抛错)。
禁用
在 profile 的 cordis.patch.yml 里按插件行 id skill-manager 覆盖:
- id: skill-manager
disabled: true
快速上手
# 1) 装 + 重启
dsh plugin --profile web add @zfdx123/dsh-skills-manager
# 2) 打开 设置 → 技能管理:列表顶部就是诊断摘要
# 「N 个技能 · 正常 x · 警告 y · DSH 会跳过 z」
# 有 broken 的先在这一屏修掉——那些技能官方 provider 只会 warn 然后丢掉
# 3) 侧栏左下「技能」:就地浮层,随手开关或改正文,不用跳进设置页
# 4) 想加自己的技能库:设置页「自定义技能文件夹」→ 填目录(或用「浏览…」)
# 仓库式技能库(技能在 <集合>/skills/<名称>/SKILL.md)记得勾「递归」
让模型用它:
用 skill_manager 列出所有技能,把 DSH 会跳过的那些连同原因一起告诉我。
用 skill_manager 新建一个技能:name=changelog-entry,description=…,写进当前项目根目录。
它做什么
官方 provider 缺的那一半
官方 provider(@deepseek-ai/dsh-skill-filesystem)负责发现并加载,但它有几个让技能作者很难受的特性:
| 官方行为 | 后果 |
|---|---|
坏 frontmatter 只 logger.warn,然后静默跳过 |
模型侧分不清「技能不存在」和「技能写坏了」 |
| 会话目录里看不到路径、层、rank、遮蔽关系 | 改了文件不生效时,不知道是哪一份在生效 |
| 完全没有写操作 | 新建/改名/开关/搬运只能靠手改文件 |
name 语法、布尔语法、遗留键都是硬规则 |
写错了没提示,技能直接消失 |
本插件补上这些:诊断 + 读写 + 分层可视化。
交付了什么
| 面 | 位置 | 说明 |
|---|---|---|
| 设置页 | 设置 → 技能管理 | 完整管理台:列表 / 诊断 / 详情编辑 / 新建 / 搬运 / 回收站 |
| 侧栏入口 | 侧栏左下角 技能 | 就地浮层,随手开关或改正文 |
| 模型工具 | skill_manager |
让模型在对话里给某类任务现场造技能或修技能 |
| 宿主 API | /api/skills/manager |
上面三者共用的唯一数据通道,带回环来源护栏 |
诊断(核心价值)
- 列出磁盘上全部技能,包括官方会静默跳过的那批,并说明会被跳过的原因;
- 按官方语法逐项校验:
namekebab-case、description必填、布尔语法(boolean | 1 | 0 | "1" | "0" | true/false/yes/no/on/off,大小写不敏感)、遗留键(disableModelInvocation/modelInvocable/userInvocable会让 DSH 整条丢弃该技能); - 标注遮蔽关系:同名多副本时谁是胜者、谁被遮蔽、为什么;
- 与
ctx.skills注册表交叉核对,指出「磁盘上有但 DSH 还没认」的技能; - 提示
frontmatter.name与文件/目录名不一致(改名请用「重命名」)。
编辑
- 新建:名字 + 一句话说明 + 目标根目录 + 形态(目录包 / 单文件),自动写入可用骨架;
- 只改正文 → frontmatter 逐字节保留(注释、键顺序、本插件读不懂的结构都活着);
- 改字段 → 只重写对应那一行,其余行不动;
- 写盘前先校验:会让 DSH 跳过这个技能的内容拒绝落盘,避免把好文件改坏;
- 并发保护:
expectedText不匹配(文件被外部改过)时拒绝覆盖。
生命周期
- 开关:一键切换「模型可见」/「
/可见」(写官方支持的disable-model-invocation/user-invocable); - 重命名:连同 frontmatter 的
name一起改,不会留下名字与文件不符的坏状态; - 移动 / 复制:跨根目录搬运,复制时同步改写副本的
name; - 删除:移入
<root>/.trash/<时间戳>-<名字>,可恢复;点开头目录天然不会被 provider 扫到。
自定义技能文件夹
- 把任意目录加成技能根(rank 300):设置页有输入框 + 「浏览…」(调宿主原生目录选择器;宿主没有
directoryPickerController时会明确提示手填路径); - 两种扫描方式,每个文件夹独立切换:
- 一层(默认,与 DSH 官方一致)——只认
<目录>/<名称>/SKILL.md与<目录>/<名称>.md; - 递归——继续向下最多 3 层找
SKILL.md,用于仓库式技能库:技能在<集合>/skills/<名称>/SKILL.md这种更深的层级时,一层扫描只会认出顶层几个;
- 一层(默认,与 DSH 官方一致)——只认
- 递归的护栏:跳过
node_modules与点开头目录;命中一个技能后不再下探(它内部的嵌套SKILL.md属于该技能的资源,如references/,不是独立技能); - 目录不存在会被拒绝;勾选创建后只在上级目录已存在时创建,不会递归造一棵目录树;
- 自动去重:重复添加、或该目录已经是内置技能根,都会被明确拒绝;
- 移除只解除管理,绝不动目录与其中的技能文件;
- 遮蔽关系即刻生效:自定义目录是 rank 300,低于项目层(100/200)、高于用户层(400/500);
- 设置写在
skill-manager命名空间;多窗口并发添加时有 revision 冲突重试,不会互相覆盖。
把管理的技能真正交给 DSH
- 注册一个技能提供者(
skills-manager,与官方的filesystem/ 保留名不冲突),管理器里的技能因此进入 DSH 会话目录、模型可按名加载; - 为什么必须有这一层:官方 provider 只扫它自己配置里的根(组合文件里那一行的
customSkillDirs),管理器面板添加的目录它根本不知道——不注册提供者的话,面板列出的技能就是「看得见、用不上」; - 提供者与面板共用同一套发现逻辑,所以递归目录里的技能也一并生效;
- 每次写操作(新建/改/开关/搬运/删除/增删自定义文件夹)之后立刻让 DSH 重扫目录:注册表的目录缓存按 revision 键控,而本提供者没有文件 watcher,不主动失效的话面板里删掉的技能仍被广告、刚建的进不去;
inventory: false可关掉登记(只想看、不想让模型加载到);开关是动态的——提供者每次list()现读配置,改完立即生效,不用拆装 provider。
安全
- 所有写操作必须落在已解析出的可写根目录之内;
- 路径段禁止
..,点开头目录(.git/.system/.trash内部)一律拒绝; - bundled 根(安装产物)只读;
- 包含关系判定按最严格语义实现:跨盘符(Windows 上
path.relative()会返回绝对路径)、同级目录、父目录、自身、空值输入一律判「不在里面」——这是所有写护栏的地基; - 原子落盘:同目录临时文件 + rename,避免半截文件被 provider 读到;
- HTTP 接口只接受本机回环来源,且 Host 头必须指向本机(挡 DNS rebinding)。
界面:与外壳同一套原生组件
- 弹窗、提示、按钮、输入框、开关、标签、状态点都用外壳自己暴露的 UI 原语(
@deepseek-ai/dsh-client-ui-primitives的Modal/Button/Input/Switch/Tag/StateDot/Toast),不自己抄样式,所以暗色、Esc/点遮罩关闭、焦点行为、动效都与设置页其余部分一致; - 删除 / 移除文件夹 / 重命名 / 移动 / 复制不再用浏览器自带的
confirm/prompt:删除与移除是原生确认弹窗(危险操作用外壳的红字写法),重命名与复制是带输入框的弹窗,移动/复制列出可写根目录点选,成功提示是顶部那条会自动淡出的原生 Toast; - 写入过程中弹窗关不掉(遮罩、Esc、× 全部拦下),避免写到一半状态错乱;
- 界面上的图标同样取自外壳图标集(
IconPlusOutline16/IconCheckOutline16/IconWarningOutline16/IconSkillOutline16):按钮走Button的icon通道,其余直接渲染图标节点; - 拿不到原语(或外壳版本里没有这个图标)时自动降级(退回浏览器弹窗 + 自带样式 + 原来的字形
+/✓/✖/◈),老外壳上不会白屏; - 文案跟随外壳语言:注册
skill-manager命名空间的 zh/en 两套字典(键集一致);ctx.locale缺席时回落中文; - 客户端半体是 classic script、无构建步骤,并在
dsh.client.inject里声明它依赖的设置页与侧栏 UI 包。
配置
设置命名空间 skill-manager(由宿主持久化),全部有默认值,不配也能用:
| 字段 | 默认 | 含义 |
|---|---|---|
customSkillDirs |
[] |
额外的技能根目录(对应官方 provider 的 customSkillDirs,rank 300)——只扫一层 |
deepSkillDirs |
[] |
递归扫描的技能根(rank 300):向下最多 3 层找 SKILL.md,用于仓库式技能库 |
bundledSkillDir |
'' |
捆绑技能根(rank 600,只读) |
projects |
[] |
除了进程 cwd 之外还要管理的项目目录 |
inventory |
true |
是否把管理的技能登记给 DSH(关掉则只在面板可见,模型加载不到) |
前两项就是设置页「自定义技能文件夹」卡片管理的两个列表(含每个文件夹的「一层 / 递归」开关);后三项没有图形入口,写在设置里即可,inventory 改完立即生效。
根目录解析规则与官方完全一致(rank 越小越优先):
| rank | source | 路径 |
|---|---|---|
| 100 | project-dsh |
<projectRoot>/.dsh/skills |
| 200 | project-agents |
<projectRoot>/.agents/skills |
| 300 | custom |
配置的 customSkillDirs / deepSkillDirs |
| 400 | user-dsh |
<dshHome>/skills(跳过 .system) |
| 500 | user-agents |
<agentsHome>/skills |
| 600 | bundled |
配置的 bundledSkillDir(只读) |
projectRoot = 最近的含 .git 的祖先目录;没有就退回 cwd。bundledSkillDir 为空串时不会有第 6 个根。
模型工具 skill_manager
工具用 @deepseek-ai/dsh-tools 的 defineTool 定义(该包 DSH 自带,本包声明为 optional peer):下面这张表就是参数 schema 本身,同一份声明既编译成给模型看的 JSON Schema,也在 execute 之前做参数校验——类型或枚举不符的调用会在进入业务代码前被拒(未知 action 由校验层抛错,而不是靠 execute 里的兜底分支),声明与校验不会各写一份。
| action | 必填 | 说明 |
|---|---|---|
list |
— | 列出全部技能 + 诊断;status 可过滤 ok / warning / broken |
roots |
— | 列出根目录、rank、是否可写、各有多少技能 |
read |
path |
读一条技能的完整内容与诊断 |
create |
name, description |
新建;可选 rootPath / kind / whenToUse / body / modelInvocable / userInvocable |
update |
path |
改 description / whenToUse / body |
toggle |
path, modelInvocable 或 userInvocable |
开关入口 |
rename |
path, newName |
改名 + 同步 frontmatter |
move |
path, targetRoot |
换根目录 |
copy |
path, targetRoot |
复制(可给 newName) |
delete |
path |
移入回收站 |
lint |
— | 只列有问题的技能 |
dirs |
— | 列出自定义技能文件夹(含「一层/递归」与技能数) |
addDir |
path |
添加自定义技能文件夹;create: true 时目录不存在则创建;deep: true 递归扫描 |
removeDir |
path |
从管理器移除(不改动磁盘文件) |
setDirDeep |
path, deep |
就地切换某个已添加目录的扫描方式 |
前置要求
- DSH
^0.1.7-rc.2(package.json的engines.dsh;dsh-settings/dsh-tools两个 peer 的范围同为^0.1.7-rc.2); - Node ≥ 22(
engines.node); - 装成 profile bundle(
dsh.bundle.patch→cordis.patch.yml),并在dsh.client.platform: web的前端下使用——设置页整页与侧栏入口都只在 web 前端出现; - peer:
@deepseek-ai/cordis ^4.0.4(必需)、@deepseek-ai/dsh-settings ^0.1.7-rc.2(optional)、@deepseek-ai/dsh-tools ^0.1.7-rc.2(optional); - 运行时依赖
@deepseek-ai/schemastery ^3.18.1(settings.register的 schema 必须是可调用的 schemastery 对象,所以它是运行时依赖,锁文件里必须有;test/manifest.test.js会核对); settings服务是硬依赖(inject = ['settings']),因为它提供可配置的技能根目录;- 不需要任何外部服务、端口、网络或 API key。
已知限制
侧栏入口依赖外壳的
data-slot锚点,锚点消失时 CSS 规则是静默空转的。外壳的sidebar.footer.action是一个 nowrap 的横向 flex 动作行,而dsh-memery这类邻座的入口用width:100%(里面的按钮再向外出血 4px)占满整行,会把本插件的入口推到侧栏右边缘之外、只剩一截圆角边框。插件注入的规则让这一行可以换行::has(> [data-slot="sidebar.footer.action"] > .dsh-skills-manager){flex-wrap:wrap}它从本插件注册进去的那个 slot 锚点(
data-slot,渲染器给每个 slot 出的锚点)出发,并用:has()要求本插件确实在座,所以外壳自己的其它*_footerActions行(例如 user-questions 弹窗的按钮行)不受影响,也不依赖 CSS Module 的局部名——早期版本用的[class*="_footerActions"]全局片段选择器两个毛病都有,已弃用。代价是:锚点改名、或浏览器不认:has(),规则就不命中,而且是静默的。缓解措施只在「入口真的被挤出动作行」时生效——入口挂载时会量一次几何,出界(或找不到锚点)就在浏览器控制台打一条[dsh-skills-manager]开头的警告;只要没被挤出界,规则失效不会有任何提示,入口看起来一切正常。面板读的是磁盘,不是注册表。注册表已经被 provider 过滤过一遍,用它做数据源会漏掉所有被跳过的坏技能——而那正是最需要被看到的东西。代价是面板可能列出 DSH 此刻还没加载的技能(提供者刷新延迟,界面会单独标出来);反过来,「面板里能看到」不等于「模型现在能用」。
坏 frontmatter 的判定是复刻官方语法,不是调用官方实现。官方改语法(
SKILL_NAME正则、布尔拼写集合、遗留键黑名单)而本插件没跟上时,会出现「面板说没问题、DSH 还是跳过」或反之。判据都集中在lib/logic.js,对齐的是当时的官方实现。frontmatter 是「行级手术」,不是 YAML 引擎。只改正文时逐字节保留,改字段时只重写那一行;代价是本插件读不懂的 YAML 结构(多行块标量的复杂变体、锚点/别名、嵌套对象)不会被打平或规范化——它保持原样,某些结构下的字段编辑会做不了。
写入边界是硬约束:路径段禁止
..,点开头目录一律拒绝——想把技能放进.system、.hidden之类的目录做不到;bundled 根只读;所有写操作必须落在已解析出的可写根内。这些拒绝是设计目标,不是缺陷。单文件上限 2 MiB(
MAX_SKILL_FILE_BYTES):超过这个大小的技能不会被读取,也不会被写入。并发保护是乐观锁。
expectedText不匹配(文件被外部改过)时拒绝覆盖;两个窗口在同一瞬间先后写时,后写者拿到的是新的expectedText,所以它挡的是「外部改动」而不是「同时写入」。HTTP API 的信任边界是「本机」:只接受回环来源且 Host 头指向本机,因此不能通过远程访问或反向代理访问管理界面。这是挡 DNS rebinding 的护栏,也是使用上的限制。
原生目录选择器可能不存在:宿主没有
directoryPickerController时pickDir返回supported: false,界面提示手填路径——降级而已,不影响添加目录。回收站不自动清理:删除把文件移进
<root>/.trash/<时间戳>-<名字>就结束,插件不清理、不做保留期;攒多了要自己删。没有文件 watcher:提供者只在写操作之后主动让 DSH 重扫目录缓存;你在编辑器/脚本里改了磁盘,面板要手动刷新才看得到,DSH 的目录也要等下一次写操作或它自己的重扫。
settings不可用时功能缩水:settings.register抛错时插件降级为「全部用默认配置」并记一条 warn(一个可选的技能管理器不该让整个 profile 起不来);此时改配置不生效,且设置页对只读配置会明确拒绝而不是静默失败。
开发
npm test # node --test
测试分七层(共 162 项,全绿):
test/logic.test.js—— 纯逻辑(解析、校验、序列化保真、诊断、遮蔽);test/store.test.js—— 真实临时目录上的 I/O(发现、写入保真、搬运、回收站、路径护栏);test/deepscan.test.js—— 递归扫描(层级上限、跳过node_modules/点目录、命中后不再下探);test/provider.test.js—— 技能提供者(注册表契约、动态inventory、未知 host / 失败不抛);test/assembly.test.js—— 宿主装配自检(假 Cordis 上下文:路由、工具、参数校验、护栏、优雅降级、卸载);test/client.test.js—— 客户端装配自检(假__ModuleLoader__与假 UI 原语:注册 id、Slot 参数、样式、弹窗/Toast 走原生组件、拿不到原语时的降级路径、entryWrapWarning、纯函数);test/manifest.test.js—— 依赖清单自检(锁文件必须记录 package.json 的每一个运行时依赖)。
Windows 上
node --test会为每个测试文件起子进程;若运行在受限沙箱里会报spawn EPERM,用node --test --test-isolation=none在同进程里跑即可。
代码结构
index.js 宿主半:settings 命名空间、/api/skills/manager、skill_manager 工具
client.js 客户端半:设置页整页 + 侧栏浮层(classic script,无构建步骤)
lib/logic.js 纯函数:frontmatter 解析/序列化/行级手术、校验、诊断、遮蔽、面板渲染
lib/roots.js 根目录解析与发现(对齐官方 rank 表、「只认一层」与递归扫描)
lib/store.js 受护栏保护的读写:路径校验、原子写、回收站、搬运
lib/provider.js 技能提供者:把管理器里的技能登记给 DSH(含 inventory 开关)
lib/schema.js settings 命名空间 schema(必须是可调用的 schemastery 对象)
test/ node:test 单元测试 + 宿主/客户端装配自检
cordis.patch.yml bundle patch:往组合树插入本插件的行
两个刻意的设计决定
为什么自己解析 frontmatter。 官方用 yaml 包,但那个包不在 web profile 的解析路径上,而本插件要在不新增依赖的前提下工作。更关键的是诊断:官方遇到坏 frontmatter 只 warn 然后静默跳过,本插件要把这些信息变成可展示的 issue。所以 lib/logic.js 实现了一个严格对齐官方语法的读取器(SKILL_NAME 正则、必填字段、frontmatterBoolean 的 8 种拼写、遗留键黑名单),并遵循「读不懂就不猜、原样保留」的原则。
数据源为什么是磁盘而不是 ctx.skills。 注册表只用来做交叉核对(「DSH 当前认这些」),不用来列表。
许可
MIT。完整文本见包内的 LICENSE。