dsh-skill-center(技能中心)
DSH 侧栏「技能中心」入口 + 中栏技能浏览器:顶部按来源目录分页,技能以卡片分块展示,
卡片上可直接启用/停用、通过 ⋯ 菜单打开文件夹,点击卡片弹出详情框看完整
SKILL.md;来源目录本身可在界面里增删改并持久化。

简介
- 给谁用:技能(
SKILL.md)攒了一堆、却只能靠翻文件夹才知道有哪些、开没开的人。 - 解决什么:技能散在好几个目录里,缺少统一的"看一眼 + 开关"入口;而且当 profile 里
@deepseek-ai/dsh-skill-filesystem没有激活时(常见情形),skill工具根本列不出这些 文件型技能 —— 技能放了却调不到。 - 怎么做:面板只读扫描你配置的技能目录;同时把启用中的技能注册进 DSH 的技能表
(
ctx.skills),于是开关键 = 模型能不能看到这个技能,页头徽标「已接入会话 X/Y」实时显示结果。 - 不做:不删文件、不移动、不联网、不采集任何数据;唯一的写操作只改该技能
SKILL.md里的disable-model-invocation一行(原子写、可逆)。 - 零依赖、免构建:宿主半区只用 node 内置模块,浏览器半区只用加载器给的
react, 没有第三方包、没有构建步骤,装完即用。
一眼看懂:顶部按来源目录分页 → 卡片点开看全文 → 卡片上的开关启用/停用 →「⋯」里打开文件夹。
快速开始
dsh plugin --profile <profile> add github:mwk719/dsh-skill-center # 或 link:<本地路径>
装完刷新页面即可(首次安装即时生效);只有改动 lib/index.js(宿主半区)才需要重启
DSH 应用。完整的安装 / 卸载(含原始 git URL 与 monorepo 子目录写法)、以及"改代码后怎么生效",
见下文对应章节。
默认来源目录
| 分组 | 目录 | 技能形态 |
|---|---|---|
| dsh技能 | %USERPROFILE%\.dsh\skills(即 ~/.dsh/skills) |
单文件 .md 技能 |
| workbuddy技能 | %USERPROFILE%\.workbuddy\skills(即 ~/.workbuddy/skills) |
目录 + SKILL.md |
以上只是默认值:组名(显示名)、id、路径都可在界面里随时改,也可以增删来源目录。
功能
- 侧栏入口:注册在 shell 自己的面板列表(
sidebar.panellist,order: 42)。 - 顶部页签分组:每个来源目录一个页签(显示名 + 技能数),右侧标出当前根路径与
来源标记(
内置默认/插件配置/已自定义);搜索只作用于当前页签。 - 卡片分块展示:自适应网格,每张卡是「首字母头像(同名同色)+ 技能名 + 描述
(最多 3 行)+
⋯菜单 + 启用开关」;停用的技能显示「已停用」标签并整体降透明度。 - 启用 / 停用:写
SKILL.mdfrontmatter 的disable-model-invocation(停用 → 写入true;启用 → 删掉该行,把文件还原成作者原本的样子; 文件本来没有 frontmatter 时会补一个最小块)。原子写入(同目录临时文件 + rename), 保留 BOM、原行尾风格、其余 frontmatter 字段与正文逐字节不变。 ⋯菜单(fixed 定位,不会被网格滚动裁剪):打开文件夹 / 复制路径 / 启用·停用。 「打开文件夹」由宿主调explorer.exe /select,<文件>(macOSopen -R、Linux 打开所在目录), detached + 无 stdio,不阻塞请求。- 点击卡片 → 详情弹框:头部是头像 + 技能名 + 路径 + 大小(超 512KB 标「已截断」)+
「打开文件夹」按钮;正文是
SKILL.md全文。关闭三条路径:✕/ 点遮罩 /Esc。 - 来源目录编辑器(页头「来源目录」):逐行编辑
id / 显示名 / 路径,支持新增、 删除、恢复默认;保存前由宿主校验(id 只允许小写字母数字连字符、路径必须绝对、 至少一个目录),错误就地显示且不关闭弹框。保存写入~/.dsh/skill-center.json, 此后它优先于插件config.groups。 默认只有两个来源目录(~/.dsh/skills与~/.workbuddy/skills,见上表), 但组名(显示名)、id、路径三样都能改 —— 改名只影响页签文字,路径与技能数不变; 也可以用下面的config.groups预设。 - 链接技能只读:路径上含符号链接 / junction 的技能可以列出和读取,但拒绝改写 (沿技能根逐段检测,因为 junction 里的文件本身 lstat 不是链接),开关自动置灰。
- 零依赖:宿主半区只用 node 内置模块;浏览器半区只用模块加载器提供的
react/react/jsx-runtime,无 JSX、无第三方包、不需要构建步骤。
开启的技能如何被 DSH 会话识别到
宿主半区在挂载时(以及每次「启用/停用」「改来源目录」之后)都会做一次运行时注册:
- 启用的技能 →
ctx.skills.register({ name, description, content, path, source }); - 停用 / 改名 / 文件消失的技能 → 调对应 disposer 移出注册表;
- 界面直接呈现结果:页头徽标 「已接入会话 X/Y」(X = 此刻真在技能表里的数量, Y = 启用中的总数),卡片上的 「未接入」 标签(悬停显示原因)。
为什么不用文件系统 provider:在 @deepseek-ai/dsh-skill-filesystem 未激活的 profile 里,
任何文件型技能(.dsh\skills / .workbuddy\skills / .agents\skills)都进不了
skill 工具的目录(实测);而运行时注册是 DSH 里既有的另一条通路——不少插件就是这样
把自己的技能送进模型的(例如 cleverer-dsh/plugins/dsh-skill-provider.mjs)。
走这条路的好处:不依赖 provider、覆盖任意来源目录(含 .workbuddy\skills)、
且开关直接等于「会话能否看到」,语义闭环。
细节与边界:
- 技能名优先取 frontmatter 的
name,不是 kebab-case 时退回目录名 / 文件名,都不合法才报「未接入」; - 注册内容剥掉 frontmatter(只注册正文);没写
description时用正文首行兜底(注册表要求非空); - 同名技能只注册第一个,其余标注「未接入:技能名重复」并在界面上给出原因;
source先试runtime,被注册表拒绝时自动退回bundled(官方 provider 用的取值);- 注册表变更对新会话立即生效;已开始的会话其技能目录是启动时快照的,不受影响。
技能识别口径
与官方 dsh-skill-filesystem 的目录约定一致:
- 目录 + 目录内存在
SKILL.md→ 目录型技能(kind: dir),技能名 = 目录名; - 技能根下的单个
.md文件 → 单文件技能(kind: file),技能名 = 文件名去掉.md; - 目录 / 文件是链接(junction)时同样列出,但标记
linked: true并禁止写; - 跳过:以
.开头的条目(.git/.claude/.trash_*)、README.md、 以及没有SKILL.md的目录。 - 目录里没有
SKILL.md、但有专家团清单 → 智能体 / 专家团(kind: agent),见下一节。
列表项的 description / whenToUse 取自 frontmatter(零依赖轻量解析,支持 | / >
块标量),只读文件头 8 KB,不会把 200 KB+ 的技能文件整个读进内存。
专家团 / 智能体目录形态
WorkBuddy / CodeBuddy 的「专家团(团队型专家)」是另一种目录形态:目录里没有
SKILL.md,而是自带清单 plugin.json。
判据(按真实数据校准,避免误判技能):
- 目录里有
SKILL.md→ 按技能处理(即使它同时带.claude-plugin/plugin.json, 例如社区里把技能打包成插件的web-access); - 没有
SKILL.md,而.codebuddy-plugin/plugin.json或.claude-plugin/plugin.json里声明的agents[]非空 → 按智能体处理(kind: agent); - 清单解析失败 → 仍作为智能体列出,并给出「未接入原因:智能体清单无法解析」
(
list不会因此 500); - 只声明
skills: ["./"]的技能型插件清单、且目录里没有SKILL.md→ 跳过。
显示什么(清单字段 → 列表项,文案一律 zh → en → 回退 name / description):
| 清单字段 | 卡片 / 详情弹框 |
|---|---|
displayName |
卡片标题(回退 name → 目录名) |
displayDescription |
卡片描述(回退 description) |
profession |
详情弹框头部的职衔 |
tags[] |
卡片与弹框的标签 chips(也参与搜索) |
quickPrompts[] |
弹框「团队帮你做」逐条 |
members[](displayName / profession / avatar / role) |
弹框「团队成员」:各自头像 + 名字 + 职衔/角色;卡片上显示「N 人团队」 |
expertType(team)、agents[] |
列表项字段;agents[] 作为「N 份提示词」的来源 |
avatar / members[].avatar |
头像图,走 GET /avatar 服务 |
agents[] 里与 agentName 同名的那份 |
lead 提示词:注册进会话技能表的正文,也是详情弹框的正文 |
开关 = 内存态,绝不写文件:这类目录没有 disable-model-invocation 可改,所以
开关只存进来源目录的持久化文件(~/.dsh/skill-center.json)的新字段:
{
"version": 1,
"groups": [{ "id": "dsh", "label": "dsh技能", "root": "C:\\Users\\<用户名>\\.dsh\\skills" }],
"agents": { "dsh": { "<专家目录名>": true } }
}
agents 字段缺失 = 默认启用,老文件(没有这个字段)照读不误。改开关时属性目录里的
任何文件都逐字节不变(测试锁死了这一点)。启用时,lead 提示词会以 agentId 为技能名
注册进 DSH 会话的技能表;停用即 dispose —— 与会话可见性的语义和技能完全一致。
头像路由:GET /api/skill-center/avatar?group=<gid>&id=<agentId>&which=lead|<memberId>。
which 只是查表键,服务端从本次扫描结果里取绝对路径,并二次校验「必须落在该
agent 目录内」+ 扩展名白名单(png/jpg/jpeg/webp/gif/svg),查不到一律 404 ——
客户端传什么都不可能读到目录外或被当路径使用。
路由
| 路由 | 方法 | 说明 |
|---|---|---|
/api/skill-center/list |
GET | 技能 + 智能体清单(含 enabled / kind / folder / linked)+ source |
/api/skill-center/read?group=&id= |
GET | 单个技能正文(智能体 = lead 提示词;上限 512 KB) |
/api/skill-center/set-enabled |
POST | 启用/停用,body {group,id,enabled}(智能体只写 store) |
/api/skill-center/reveal |
POST | 在系统文件管理器中定位,body {group,id} |
/api/skill-center/avatar?group=&id=&which= |
GET | 专家团头像(which=lead 或成员 id;仅图片,404 兜底) |
/api/skill-center/groups |
GET | 当前来源目录 + source + storePath + defaults |
/api/skill-center/groups |
POST | 改写来源目录({groups:[...]})或 {reset:true} 恢复默认 |
浏览器半区用文档相对路径(api/skill-center/...,无前导斜杠)请求:GUI 以
<base href="./"> 提供服务,根绝对路径会逃出子路径部署前缀。
安全模型
- 七条路由都只接受同源 loopback 请求:socket 远端必须是 loopback(127/8、
::1、 IPv4-mapped),Host必须是 loopback authority,Sec-Fetch-Site: cross-site拒绝,Origin存在时必须与Host同源;X-Forwarded-For从不信任。 - 写路由(set-enabled / reveal / groups)与读路由身份只认「最新一次扫描」解析出的
路径:客户端传来的
path从不作为凭据,同名但已消失/换位的技能不会被改到别处文件。 - 符号链接 / junction 一律拒绝改写(
400 linked skill cannot be modified): 原地改写 + rename 会越出技能根。检测沿技能根逐段 lstat,而不是只看最终文件。 - 请求体上限 256 KB;
groups的每一行都做 id / label / 绝对路径 / 去重校验。 - 预览用文本节点渲染(
<pre>{content}</pre>),无 HTML 注入。
隐私
- 零遥测、零外发:宿主半区没有任何对外网络请求;唯一的进程拉起是你在
⋯菜单点 「打开文件夹」时才执行的本地文件管理器命令。浏览器半区只请求本插件自己的文档相对路由。 - 零凭据:不读取、不保存任何 token / 密钥 / 账号信息。
- 落盘只有一处:你自己在「来源目录」里保存的
%USERPROFILE%\.dsh\skill-center.json(就是来源目录列表)。其余一切都只是只读扫描你的技能目录。 - 写操作只在你明确点击时发生:启用/停用只改该技能
SKILL.md里的disable-model-invocation一行(原子写、可逆、已停用时不做无谓写入),不删任何文件。
安装 / 卸载
# 从 GitHub 安装(推荐)
dsh plugin --profile <profile> add github:mwk719/dsh-skill-center
# 也可以直接用仓库 URL(国内访问 GitHub 慢时,可套一层代理,例如把
# https://github.com/... 替换成 https://gh-proxy.com/https://github.com/...)
dsh plugin --profile <profile> add https://github.com/mwk719/dsh-skill-center.git
# 本地开发:link 到本仓库源码
dsh plugin --profile <profile> add link:<仓库路径>/dsh-skill-center
# 卸载
dsh plugin --profile <profile> remove dsh-skill-center
也可以走插件市场:dsh plugin --profile <profile> add dshmarket,然后在 Settings → Plugins 里一键装。
安装后 profile 的 package.json 会多出 "dsh-skill-center": "github:mwk719/dsh-skill-center"
(link 安装则是 "link:<仓库路径>/dsh-skill-center"),并在 dsh.profile.bundles 追加
dsh-skill-center;bundle 行由本包 cordis.patch.yml 插入(id: skill-center)。
安装动作对 live profile 即时生效(首次安装无需重启)。
⚠️ 改代码后的生效方式
| 改动 | 生效方式 |
|---|---|
lib/client.js(界面) |
刷新页面(F5)——客户端 bundle 按请求从该文件现取现供 |
lib/index.js(宿主路由) |
必须重启 DSH 应用,否则跑的还是旧模块 |
宿主侧不能热更新:loader 对同一模块 URL 走 ESM 模块缓存,set_plugin 重放条目、
remove_bundle + install_bundle 都不会重新 import(本机实测三种方式均仍是旧代码,
/list 里没有新字段、新路由返回 401)。重启后无需其它操作。
测试
node tests/host-scan.test.mjs # 宿主:扫描口径 + 夹具写入 + 围栏/错误路径
node tests/skill-bridge.test.mjs # 会话可见性 ↔ 启用开关(运行时注册桥)
node tests/client-render.test.mjs # 端到端:伪 loader/react/slots 渲染真实客户端半区
node tests/default-groups.test.mjs # 默认来源目录:WorkBuddy 专家市场发现 + "扫到有才加" 过滤
三个测试都用 tests/_harness.mjs,且全部夹具驱动:写操作落在 mkdtemp 出来的
临时目录里(DSH_SKILL_CENTER_STORE 指向临时 store、DSH_SKILL_CENTER_NO_SPAWN=1
让 reveal 只回命令),断言里不写死任何本机技能名或数量——因此跑测试既不碰真实技能,
也能在任何机器上通过。
覆盖:目录扫描口径(含 junction / 无 frontmatter)、停用写入与启用还原、 符号链接拒绝改写、reveal 命令构造、来源目录读写与校验与优先级、 页签分组、卡片数与开关、⋯ 菜单三动作、详情弹框与三条关闭路径、搜索过滤、 专家团(agent)识别与字段口径、坏清单不崩、头像路由与目录穿越防护、 agent 开关只写 store(目录文件逐字节不变)、store 向后兼容。
client-render 的伪 hooks 会按 deps 重跑 effect 并执行 cleanup(贴近 React),
因此 document 级 Esc 监听也被真实驱动;所有断言都基于夹具条目(alpha / beta /
expert(迷你专家团)/ linked / 重名夹具),不依赖任何本机技能库内容。
配置(可选)
config.groups 可作为来源目录的默认值(被界面里保存的 ~/.dsh/skill-center.json
覆盖);不配置也不影响使用:
- id: skill-center
name: 'dsh-skill-center'
config:
enabled: true
groups:
- id: dsh
label: dsh技能
root: 'C:\Users\<用户名>\.dsh\skills'
实测结论(2026-09-30 · 重启后)
node tests/live-verify.mjs 对着运行中的 GUI 打真实 HTTP(本机实测全部检查通过):
- 宿主半区已加载新代码:
list返回source=default,技能字段含kind / folder / enabled / disabled / linked;默认两组dsh技能/workbuddy技能(数量随各人技能库而定)。 GET/POST /groups:保存后source=file并写出%USERPROFILE%\.dsh\skill-center.json;reset后回到default两组。POST /set-enabled:真实写入disable-model-invocation: true(正文与其他 frontmatter 字段逐字保留),list立刻反映enabled=false;再启用后文件逐字节还原。POST /reveal:返回explorer.exe /select,<…>\live-demo\SKILL.md,真的弹出了资源管理器。- 客户端
sidebar.panellist与main两个座位的skill-center(order: 42)均为active: true。
验证脚本只在系统临时目录里新建夹具技能,两个真实技能目录仅被读取;
结束时一定会 reset 还原来源目录(用 finally 兜底)。
注:若同时安装了其它同样注册
sidebar.panellist/main的技能面板插件(例如@linxin666/dsh-client-ui-skill-explorer),侧栏会出现两个同名「技能中心」入口, 改一个 id 或停用其一即可。
会话可见性(运行时技能桥)
- 启用中的技能全部注册进
ctx.skills(source=runtime,无重名冲突时 100% 注册)。 - 停用 → 立刻退出会话技能表(
registered=false且 disposer 已调);启用 → 立刻挂回。 - 无 frontmatter 的单文件技能(fixture
beta.md):正文兜底出description后正常注册。 - 同名技能:只注册第一个,另一个带原因(界面显示「未接入」)。
source兼容性:把注册表限制为只接受bundled时自动退回,仍全部注册成功。- 认证方式:
node tests/skill-bridge.test.mjs(无需浏览器/无需重启)。
常见问题
安装时报 git@github.com: Permission denied (publickey)
github:owner/repo 这种简写(不带 ref)在安装时,pnpm 会用 SSH 去 git ls-remote
解析默认分支;这台机器若没把 SSH 公钥注册到 GitHub,就会在这里失败(exit 128,
application: failed)。三种解法任选:
# ① 固定 commit(推荐,免掉 ls-remote 解析)
dsh plugin --profile <profile> add github:mwk719/dsh-skill-center#<40 位 commit sha>
# ② 直接用 HTTPS 的 git URL
dsh plugin --profile <profile> add https://github.com/mwk719/dsh-skill-center.git
# ③ 给 GitHub 加一把 deploy key(仓库 Settings → Deploy keys),让 SSH 这条路也通
排查前先确认 HTTPS 可达:git ls-remote --heads https://github.com/mwk719/dsh-skill-center.git。
⚠️ 卸载前先备份:remove 成功而紧接着 add 失败,插件就停在"已卸载"状态
(侧栏入口与技能桥一起消失)。先备份 profile 的 package.json 或记住原来的依赖串,
失败时用同一条 add 还原即可 —— 来源目录与每个开关的状态都存在
~/.dsh/skill-center.json,卸载不会丢。
组件启动时默认读取哪些目录?
首次启动(尚无 ~/.dsh/skill-center.json)有两个固定内置来源目录:
| 组 | 默认路径 |
|---|---|
dsh技能 |
~/.dsh/skills |
workbuddy技能 |
~/.workbuddy/skills |
在此基础上,默认态还会发现 WorkBuddy 专家市场:扫 <WorkBuddy 根>/plugins/marketplaces/*/plugins,
每个存在的市场成为一个可选来源目录:
| 市场目录名 | 生成的组 id | 显示名 |
|---|---|---|
experts |
workbuddy-experts |
workbuddy专家 |
my-experts |
workbuddy-my-experts |
workbuddy专家(我的) |
cb_teams_marketplace |
workbuddy-cb-teams-marketplace |
workbuddy团队 |
| 其它市场 | workbuddy-<市场名 slug> |
workbuddy专家(<市场名>) |
(组 id 只允许小写字母/数字/连字符,所以市场名里的 _ 等字符一律折成 -。)
"扫到有才加":可选组只有在它里面真的存在智能体(kind:'agent')时才被注入 ——
判据是智能体数而不是总条目数,所以纯插件市场(例如 codebuddy-plugins-official,只有技能型
插件)不会被带进来;市场目录不存在时也不会出现空组。两个固定组不参与这个过滤
(即使为空也照常显示,行为与以前一致)。
<WorkBuddy 根>= 环境变量WORKBUDDY_HOME;未设置时取~/.workbuddy(测试注入点)。- 发现只在默认态(
source=default)发生。一旦你在面板里保存过来源目录,就完全以~/.dsh/skill-center.json为准(面板右下角显示「内置默认 / 已自定义」),不再注入任何可选组。 GET /groups的defaults与{reset:true}用的是同一份过滤后的列表 —— "重置后看到的"就是"重置后生效的"。
技能与智能体都从每个来源根下扫描:
<root>/<目录>/SKILL.md—— 目录型技能<root>/<文件>.md—— 单文件技能(根下一层)<root>/<目录>/.codebuddy-plugin/plugin.json(agents[]非空,且该目录没有SKILL.md) —— WorkBuddy 专家团 / 智能体
默认不含 ~/.agents/skills(DSH 的官方用户技能根)—— 想让它生效,在面板「来源目录」里
加一行,或把里面的技能链接进上面两个根之一。
No comments yet. Be the first to write one.