DeepSeek Harness 的技能浏览器。安装前先做检查(名字不合法的会被 DSH 静默跳过),装下来的是某一次 commit 而不是分支。
31.5/100
综合分
2
GitHub 星标
167
npm 周下载
—/5
用户评分
数据截至 2026/10/11 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-skill-centernpm 源 · dsh-skill-center 的发布者账号已校验与本仓库一致
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · market
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 21 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-skill-center @ 0.4.1
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖要求 >=0.2.0-rc.2 · 最新 0.2.1-rc.2 兼容
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/19 22:24:26
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件发起外部网络请求
查看扫描证据(共 65 处,展示前 39 条)
- 发起网络请求client/client.js:1010
const response = await fetch(api(`/dsh-skill-center/api${route}`), { cache: 'no-store' }) - 发起网络请求client/client.js:1018
const response = await fetch(api(`/dsh-skill-center/api${route}`), { - 发起网络请求lib/net.js:51
const response = await fetch(url, { - 读取本地文件lib/net.js:124
await readFile(path) - 读取本地文件lib/net.js:167
const parsed = JSON.parse(await readFile(path, 'utf8')) - 写入本地文件lib/net.js:187
await mkdir(dirname(path), { recursive: true }) - 写入本地文件lib/net.js:189
await writeFile(temporary, JSON.stringify({ value, storedAt: entry.storedAt, ttlMs }), 'utf8') - 写入本地文件lib/net.js:190
await rename(temporary, path) - 读取本地文件lib/skills-dir.js:126
return (await stat(path)).isFile() - 读取本地文件lib/skills-dir.js:160
raw = await readFile(found.path, 'utf8') - 写入本地文件lib/skills-dir.js:184
await writeFile(found.path, edited.text, 'utf8') - 读取本地文件lib/skills-dir.js:210
entries = await readdir(root.path, { withFileTypes: true, encoding: 'utf8' }) - 读取本地文件lib/skills-dir.js:242
readFile(skillFile, 'utf8'), - 读取本地文件lib/skills-dir.js:243
stat(skillFile), - 读取本地文件lib/skills-dir.js:293
const entries = await readdir(directory, { withFileTypes: true, encoding: 'utf8' }) - 写入本地文件lib/skills-dir.js:479
await mkdir(target, { recursive: true }) - 写入本地文件lib/skills-dir.js:481
await rename(directory, join(target, name)) - 写入本地文件lib/skills-dir.js:483
await rename(flat, join(target, `${name}.md`)) - 写入本地文件lib/skills-dir.js:485
await writeFile( - 写入本地文件lib/skills-dir.js:518
await mkdir(resolve(target, '..'), { recursive: true }) - 删除本地文件lib/skills-dir.js:545
if (info.isSymbolicLink()) await unlink(candidate) - 删除本地文件递归lib/skills-dir.js:546
else await rm(candidate, { recursive: true, force: true }) - 删除本地文件递归lib/skills-dir.js:718
await rm(source, { recursive: true, force: true }) - 删除本地文件lib/skills-dir.js:721
await rm(join(destination, TRASH_META), { force: true }) - 删除本地文件递归lib/skills-dir.js:737
await rm(trashRoot, { recursive: true, force: true }) - 删除本地文件递归lib/skills-dir.js:742
await rm(target, { recursive: true, force: true }) - 读取本地文件lib/agents.js:140
const [text, stats] = await Promise.all([readFile(candidate.skillFile, 'utf8'), stat(candidate.skillFile)]) - 读取本地文件lib/agents.js:140
const [text, stats] = await Promise.all([readFile(candidate.skillFile, 'utf8'), stat(candidate.skillFile)]) - 读取本地文件lib/agents.js:177
entries = await readdir(root.path, { withFileTypes: true, encoding: 'utf8' }) - 读取本地文件lib/agents.js:255
const content = await readFile(source.skillFile, 'utf8') - 读取本地文件lib/provenance.js:99
const raw = await readFile(join(root, PROVENANCE_FILE), 'utf8') - 写入本地文件lib/provenance.js:119
await mkdir(dirname(path), { recursive: true }) - 写入本地文件lib/provenance.js:121
await writeFile(temporary, `${JSON.stringify({ version: VERSION, skills: data.skills ?? {} }, null, 2)}\n`, 'utf8') - 写入本地文件lib/provenance.js:124
await rename(temporary, path) - 读取本地文件lib/updates.js:110
const text = await readFile(record.from, 'utf8') - 读取本地文件lib/usage.js:117
const projects = await readdir(sessionsDir, { withFileTypes: true }).catch(() => []) - 读取本地文件lib/usage.js:120
const inside = await readdir(join(sessionsDir, project.name), { withFileTypes: true }).catch(() => []) - 读取本地文件lib/usage.js:142
info = await stat(path) - 读取本地文件lib/usage.js:156
bytes = await readFile(path)
扫描于 2026/10/31(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 一键安装技能
- 安装前体检
README
由 DeepSeek 最新模型翻译生成技能中心 · dsh-skill-center
一个 DeepSeek Harness 插件,在侧边栏里搜技能,看清了再装。
dsh plugin --profile <你的 profile> add dsh-skill-center
装上之后侧边栏底部会多出一个「技能中心」按钮:
浏览技能
| 同名冲突 | 本机导入 |
| 占名、缺件、不完整、名字不合法:四种坏法各配自己的后果与选项 | 扫描 35 个别的 agent 技能目录,把 DSH 看不见的技能导进来 |
| 已安装 | 深色主题 |
| 装完之后的状态:来路、更新检查、调用开关、回收站 | 颜色全走 --dsw-alias- 令牌,没有第二套样式表 |
面板分三块。发现聚合 5 个来源的技能目录,能按中英文关键词搜,也能按分类和 star 排序。已安装直接读你本机的 ~/.dsh/skills,把每个技能的来源、文件数和校验结果列出来,并且可以就地开关(关掉不等于删掉,见下面「关掉一个技能」),还能告诉你每个技能被模型调用过几次。本机扫的是 35 个别家 agent 的目录,把 DSH 看不见的技能导进来。
想看清楚再动手就进详情页:仓库目录树、SKILL.md 原文、逐个文件预览。确认后写到 ~/.dsh/skills/<name>/,harness 的 chokidar 立刻侦测得到,不用重启。
展开完整文档(README 剩余 1 节)
技能中心 · dsh-skill-center
一个 DeepSeek Harness 插件,在侧边栏里搜技能,看清了再装。
dsh plugin --profile <你的 profile> add dsh-skill-center
装上之后侧边栏底部会多出一个「技能中心」按钮:
浏览技能
| 同名冲突 | 本机导入 |
| 占名、缺件、不完整、名字不合法:四种坏法各配自己的后果与选项 | 扫描 35 个别的 agent 技能目录,把 DSH 看不见的技能导进来 |
| 已安装 | 深色主题 |
| 装完之后的状态:来路、更新检查、调用开关、回收站 | 颜色全走 --dsw-alias- 令牌,没有第二套样式表 |
面板分三块。发现聚合 5 个来源的技能目录,能按中英文关键词搜,也能按分类和 star 排序。已安装直接读你本机的 ~/.dsh/skills,把每个技能的来源、文件数和校验结果列出来,并且可以就地开关(关掉不等于删掉,见下面「关掉一个技能」),还能告诉你每个技能被模型调用过几次。本机扫的是 35 个别家 agent 的目录,把 DSH 看不见的技能导进来。
想看清楚再动手就进详情页:仓库目录树、SKILL.md 原文、逐个文件预览。确认后写到 ~/.dsh/skills/<name>/,harness 的 chokidar 立刻侦测得到,不用重启。
侧边栏按钮、shell.overlay 抽屉、设置页里的内联分区,三个入口共用同一份状态。
装得上,不等于 harness 会认它
这个插件大部分代码在处理这两者之间的落差。
装之前先告诉你它会怎么坏
| 检查 | 不做的话会怎样 |
| frontmatter 体检 | name 不合法(PDF-Processing、my_pdf_tool)的技能会被 harness 静默跳过。装完看起来成功,列表里什么都没有。面板会直说「照现在这样装上去,DSH 会直接忽略它」;能一键改写 name: 行的就改,缺 frontmatter 或缺 description 的拒绝安装,不装作成功 |
| 完整性 | 一次 429 或超时会让仓库遍历半途而废。技能看起来干净,缺的却是它自己让模型去跑的脚本。详情页标出「完整 / 部分」,并列出停止取回的原因和条数 |
| 引用缺件 | SKILL.md 里 scripts/build.py 或 schema 指向的文件如果不在取回的目录里,会被列出来。这类问题只在模型第一次真用它时才爆,而且爆在别人的任务中间 |
| 同名冲突 | 目标名字已被占用时不静默覆盖,也不静默加 -2。两种做法都是你事后才发现的性质。三个选项连后果一起摆出来:覆盖(先把现有版本完整挪进 .backup/)、改名装为 pdf-2、跳过。默认永远是不动现有的那个 |
装下来的就是那一次提交
安装读的是 commit,不是分支名:
一次请求 api.github.com/repos/{owner}/{repo}/commits/{branch} → 拿到 commit SHA 一次请求 codeload.github.com/{owner}/{repo}/tar.gz/{sha} → 整棵树,不可变
分支名是可变引用,commit 是不可变引用。raw.githubusercontent.com 对分支名有 max-age=300 的 CDN 缓存,所以「刚才看是这版、五分钟后装下来是上一版」是可能的;按 SHA 取,缓存命中永远是对的。本项目自己撞过这个坑:推完 README 立刻校验,读到的还是上一版。
详情页会写它是哪一版:「上游版本 8ca22db · 最后提交 2026-09-25」。问不到版本时(匿名 API 配额是 60 次/小时)它会明说「没能问到上游版本,这次抓的是分支当前内容」,而不是假装钉住了。
两条抓取路径都按 commit 读。主路径是一次 codeload 归档,归档不可达时回落到逐文件抓取,但回落也读那个 commit。否则回执上会写着一个它从没读过的版本,下一次更新检查就会拿错东西去比。只有连 commit 都问不到才读分支,那种情况下回执不写版本,界面也照实说。
一次请求拿整棵树,不是每个文件一次:80 个文件就是 80 次请求,也更容易在中途失败。读过的 commit 只下载一次,键就是 commit,所以这条缓存永远不会过期,也不需要失效:换个版本就是换了个键,不是这个键下的旧值。
二进制文件不再被写坏。旧路径把所有内容按 UTF-8 解,技能里的 PNG / PDF 会被静默损坏;现在按字节判断,非文本走 base64 存盘。解 tar.gz 是手写的约 60 行,用 Node 自带的 zlib,运行时依赖仍然是 0。
装完之后看得见的状态
每个装过的技能都记住自己来自哪个源、哪个仓库、哪条路径、哪个 commit,列表里直接显示。
更新检查分两步。先问分支现在的 commit,和记录里的一样就到此为止,一个文件都不用下,而且结论是确定的(同一个 commit 命名同一棵树)。不一样才去取那一版,算整棵树的哈希:
- 树也一样 → 最新,同时把记录里的 commit 前移,下次检查又变回一次小请求
- 树不一样 → 有更新
这里和旧行为最实质的差别是哈希的范围:从只算 SKILL.md 变成整棵树,所以上游改一行 scripts/.py 也瞒不过去,而这恰恰是技能最常被修的地方。拿不到版本信息时会降级成只比 SKILL.md,并在结果里标出这次的结论只覆盖了文档(depth: document)。
删除是移进 .trash/ 而不是抹掉,并弹出可撤销提示。被删的技能连同它的来路记录一起进桶,恢复后仍是「受管理」的技能。同名覆盖时,上一版完整保留在 <root>/.backup/<name>-<时间戳>/。
.trash 与 .backup 都带点前缀,是刻意的:harness 靠「目录里直接含 SKILL.md」判定技能,所以它两个都看不见,不会被当成技能列出来。
安装
已发布到 npm:dsh-skill-center
dsh plugin --profile <你的 profile> add dsh-skill-center
包名会从 npm 上解析,和你装任何别的 DSH 插件是同一条路。桌面版自带 CLI 的路径随安装位置变化, Windows 上在 <安装目录>\resources\runtime\cli\bin\dsh.cmd。
刚发版的那 24 小时里,请把版本号写上:
dsh plugin --profile <你的 profile> add dsh-skill-center@0.4.0
pnpm 11 自带一个发版冷却期(内置的 minimumReleaseAge,默认 24 小时,不写在任何配置文件里,
pnpm config get minimumReleaseAge 返回 undefined)。在这段时间里裸包名不会解析到 latest,而是解析到窗口之外最新的那一版,然后把这个范围写进 profile 的 package.json。所以 0.4.0 发布后跑上面第一条命令,装到的其实是 0.2.2——命令成功、输出干净、没有任何提示。写出版本号就绕开了, --config.minimumReleaseAge=0 也可以。这条是实测出来的,不是推测:docs/probe-npm-install.mjs 一直打印「陌生人装得上」,直到有人去看它到底装到了哪一版——它装的是 0.2.2。
想改用源码(要改代码、或想跑本仓库里那套检查):
git clone https://github.com/tuoLuoSuan/dsh-skill-center.git
dsh plugin --profile <你的 profile> add "<仓库路径>"
--profile desktop 会被普通 CLI 拒绝(profile "desktop" is managed exclusively by the Electron application)。要让桌面版自己的 profile 生效,得用上面那条 Electron 附带的 dsh.cmd,或者直接在应用内的插件管理界面里添加。
只要 profile 的 package.json 里 dsh.profile.bundles 有了本插件,且 cordis.patch.yml 是纯 insert(本仓库就是),就能热挂载,不需要重启。此后客户端代码改动同样热重载(刷新页面即可); lib/ 下的宿主代码改动仍需要重启。
本包没有任何运行时依赖:dependencies 是空的,装下去的就是 lib/、client/、locale/ 和两个清单文件,23 个文件、115.9 kB 打包(解开 383.7 kB)。对 @deepseek-ai/dsh 的依赖写在 peerDependencies 里(>=0.2.0-rc.2)并标了 optional。它是一道版本门禁,不是要去安装的东西:DSH 读这个字段判断插件和当前运行时兼不兼容,pnpm 则因为 optional 不会去装第二份宿主。
怎么确认装上了
插件本身会告诉你它有没有活着,不用去翻日志:
1. 侧边栏底部多出一个「技能中心」按钮(注册在 sidebar.footer.action 槽)。
2. 点开抽屉后,左下角「发现」标签页里的来源轨应当列出 5 个来源。如果它报「宿主代码还是旧版本」,说明宿主那一半没加载。lib/ 是宿主代码,加完插件要重启一次 DeepSeek Harness;之后改 client/ 就只需要刷新页面。
3. 面板底部的条数必须是实时数。这个数来自 GET /dsh-skill-center/api/sources,不写死。看到 0 或者一直转圈,那是上游没连上,不是装错了。在浏览器里直接开 http://127.0.0.1:<端口>/dsh-skill-center/api/sources 就能看到原始 JSON 和真实错误。
0.2.0-rc.2 上验证过。更低版本会被上面那道版本门禁拦住(见「兼容性」)。
配置
在 profile 的插件配置里可选地提供:
| 键 | 默认值 | 说明 |
| dshHome | $DSH_HOME 或 ~/.dsh | 技能根目录的父目录 |
| cacheDir | <dshHome>/skill-center/cache | 上游响应缓存 |
| trustedHosts | [] | 反代/隧道场景下额外放行的 Host |
| skillsmpApiKey | 空 | 提高 skillsmp 的每日配额 |
| agentHome | 真实 OS home | 其他 agent 技能目录的父目录(测试用覆盖项) |
关掉一个技能,但别把它删掉
「已安装」那一页每个技能右边有一个开关:关掉之后技能还在原处、还能读、还能再打开,只是模型不会再看到它。列表按来源分组,每组标题右边还有一个总开关(全组启用 / 全组禁用),关掉的技能挂一个 已禁用 标签,页头汇一个总数。
开关改的是技能自己 frontmatter 里的 disable-model-invocation——就是宿主真正读的那个字段,不是把文件改名或搬走。这么选换来三件事:
- 技能待在原地,所以列表还列得出来、正文还打得开;改回来只是删掉一行,文档逐字节回到原样。
- 对一个不是本插件装的技能一样能用。
- 技能目录在 git 仓库里时,改名会显示成一次删除,改一行不会。
写入是逐字节编辑 frontmatter 那一段(setFrontmatterFlag),不是把 YAML 解析出来再重新序列化:手写的注释、缩进、键顺序都留着。docs/probe-toggle.mjs 问的正是这件事,而且问三遍——翻过去是不是宿主认的那个「关」、翻回来是不是原来那份字节、以及一份它改不了的文档是不是被拒绝而不是写坏。
改不动的会被拒绝,并且说出是哪个字段:frontmatter 里用了宿主不认的旧写法时,宿主是整份丢掉这个技能的,那么给它写开关就等于报告了一次谁也看不见的变化。
只在三个根上生效:~/.dsh/skills、<工作区>/.dsh/skills、<工作区>/.agents/skills。
其他 agent 的技能目录(~/.claude/skills 等)只读。
请求里 enabled 是必填的布尔值,不从当前状态推断:先读、再决定、再写,是和这份文件的所有其他写入者赛跑,双击还会落在第二次读到的任意一边。
数得出来的调用次数
「已安装」页每个跑过的技能会挂一个「用过 N 次」,鼠标停上去显示最后一次是哪天。数据不是猜的:技能调用在会话日志里就是一次工具调用,工具名 skill,参数里带着技能名。读 ~/.dsh/sessions/ 下的 session.v4.jsonl.zstd 数出来就行。
只有一个坑,而且它很安静:那些日志是多帧 zstd——每次追加是一帧,一个文件里拼着好几帧。
zlib.zstdDecompressSync 只解第一帧就返回,不报错:一个 121,404 字节的日志会「成功」解成 284 字节,解出来还是合法 JSON。所以 lib/usage.js 自己按魔数 28 B5 2F FD 切帧、逐帧解。顺带的好处是峰值内存只有一帧,不是整个档。本机 87 个会话、64 MB 压缩,冷读一次 1.9 秒,之后每次 7 毫秒(按文件大小和 mtime 复用)。所以它是一条单独的 /usage 路由:列表先画出来,计数随后到,首屏不压在这一次解压后面。
值得先说清楚一件事,免得装上去以为坏了:这台机器上装了大约 40 个技能,历史上只调用过 6 个 (nature-writing 3 次,其余各 1 次,合计 9 次)。装了不用是常态,所以面板不把「0 次」铺在每一行上——只给真跑过的打标签,想知道哪些从来没碰过就用页头那个筛选器。页头还会直接写「用过 6 个」。
另外,「没有数据」和「一次也没用过」是两个答案,面板不合并它们。Node 22.15 之前没有 zstdDecompressSync,那时整个功能报「这个 Node 读不了会话日志」,而不是显示一排 0;日志目录不存在也一样。
「本机」到底扫哪些目录
35 个:21 个用户级(~/.claude/skills、~/.codex/skills、~/.gemini/skills、 ~/.config/opencode/skills、~/.codeium/windsurf/skills、~/.windsurf/skills、 ~/.trae/skills、~/.trae-cn/skills、~/.qoder/skills、~/.qoder-cn/skills、 ~/.lingma/skills、~/.openclaw/skills、~/.clawdbot/skills、~/.cc-switch/skills、 ~/.roo/skills、~/.codebuddy/skills、~/.workbuddy/skills、~/.copilot/skills、 ~/.cursor/skills、~/.gemini/antigravity/skills,以及 ~/.agents/skills), 14 个项目级(<项目>/.claude/skills、.codex、.gemini、.cursor、.github、 .opencode、.windsurf、.trae、.trae-cn、.qoder、.roo、.codebuddy、 .workbuddy,和没有归属者的 <项目>/skills),外加你自己在配置里加的路径。
这张表故意写得很笨:一个 agent 一行,路径照它自己文档里写的抄,相邻的绝不类推。
理由是不对称的——少一行,用户会在「本机」看到一个空列表,然后认为这台机器上没有别的东西;多一行指错了地方,readdir 要么什么也没找到(同样看不见),要么那个目录碰巧存在,于是从一个没有任何 agent 写过的目录里导入。两种情况都不报错。
这条不对称不是假想的。这张表原来只有 6 行(Claude Code、Codex、Agents、Gemini、 Antigravity 和 Claude Code 的项目级)。扩到 35 行之后,同一台机器上多扫出 17 个技能——它们在 ~/.workbuddy/skills 里,而旧表根本没写这个目录。
这不是「顺便多支持了几个工具」,是那 17 个技能在这之前一直不存在于这个面板里。
用户级的那 21 行可以用环境变量改基准目录:DSH_CLAUDE_HOME、DSH_CODEX_HOME、 DSH_GEMINI_HOME、DSH_OPENCODE_HOME、DSH_CURSOR_HOME、DSH_COPILOT_HOME、 DSH_WINDSURF_HOME、DSH_WINDSURF_USER_HOME、DSH_TRAE_HOME、DSH_TRAE_CN_HOME、 DSH_QODER_HOME、DSH_QODER_CN_HOME、DSH_LINGMA_HOME、DSH_OPENCLAW_HOME、 DSH_CLAWDBOT_HOME、DSH_ROO_HOME、DSH_CODEBUDDY_HOME、DSH_WORKBUDDY_HOME、 DSH_AGENTS_HOME。这套名字和 @michengai/dsh-skills-manager 用的是同一套,所以为那个插件配过环境变量的机器不用再配第二遍。
docs/probe-roots.mjs 问的就是这张表本身,而不是它的行为:这个 id 在这个 home 下解析到哪个路径、覆盖变量改了谁、给了环境变量它就照办、没给就一个都不看。最后一条是让这个探针在本机上仍然诚实的原因——如果哪天有人把 env 透传成 process.env,探针会在一个本身设了 DSH_CLAUDE_HOME 的机器上开始飘。
数据来源
| 来源 | 用途 | 需要密钥 |
| claudeskills.info | 主枚举源,中文可搜 | 否 |
| awesome-claude-skills | 仓库坐标表(repo + branch + path) | 否 |
| anthropics/skills | 官方技能仓库 | 否 |
| skillsmp.com | 搜索语料(匿名有每日配额) | 可选 |
| 已装 DSH 插件 | 各插件自带的技能包 | 否 |
正文一律经 raw.githubusercontent.com 取回(GitHub REST 未鉴权会 403,所以不用它)。
关于数字口径:claudeskills 的 type=skill 按仓库去重后约 1,679 条,所以面板底部显示的是这个数;条目总数(含 plugin / subagent / command / hook)与分类数会随时变动,以 GET /dsh-skill-center/api/sources 的实时返回为准,不要在别处引用一个固定的总数。
HTTP 接口
宿主侧把所有端点注册在一条 prefix 路由 /dsh-skill-center/api 下:
| 方法 | 路径 | 说明 |
| GET | /sources | 来源描述符 + 分类法 + 配额 + 已装计数 |
| GET | /installed | 扫描本机技能根,附来路记录、回收站条数与受管理计数 |
| GET | /list | ?source=&q=&kind=&category=&sort=&page=&limit= |
| GET | /agents | 扫描其他 Agent 的技能目录(~/.claude/skills 等) |
| GET | /trash | 列出回收站 |
| GET | /updates | 对所有受管理技能做一次更新检查 |
| GET | /usage | 数每个技能被调用过几次(?force=1 忽略缓存重扫) |
| POST | /item | 取详情(含 SKILL.md 原文) |
| POST | /preview | 取待安装文件清单 + 体检 + 完整性 + 引用缺件 + 同名冲突 + revision(本次读的是哪个 commit) |
| POST | /install | 落地到 ~/.dsh/skills/<name>/;conflict 取 fail(默认)/skip/rename/replace |
| POST | /remove | 移进回收站(仅限用户根,项目根只读);permanent: true 才真删 |
| POST | /restore | 从回收站恢复,连来路记录一起接回去 |
| POST | /purge | 清空回收站(或其中一个桶) |
| POST | /update | 按来路记录重新拉取并覆盖安装 |
| POST | /import-local | 从发现的 agent 目录导入(复制,不是就地注册) |
| POST | /read | 读已安装技能正文 |
| POST | /toggle | { name, enabled } 改技能 frontmatter 里的 disable-model-invocation;enabled 必填布尔 |
浏览器永远只跟宿主对话,不直连上游,所以没有 CORS 问题,密钥也不会到前端。
安全边界
- 每个请求(含 GET)都过同源校验:Host 缺失放行(非浏览器),Host 存在则必须是 loopback 或在 trustedHosts 里;sec-fetch-site: cross-site 直接拒;Origin 存在但解析失败也拒。
- 技能名必须匹配 ^[a-z0-9]+(?:-[a-z0-9]+)$;安装路径经 containedChild 校验,../ 与绝对路径一律拒绝。
- 只安装到 ~/.dsh/skills;/remove 也只动用户根,项目根下的技能目录以 writable: false 呈现。
- /toggle 是唯一的例外,它按设计会写工作区里的技能:~/.dsh/skills、<cwd>/.dsh/skills、<cwd>/.agents/skills 三个根,且只改 SKILL.md frontmatter 里的一个布尔字段,不新建、不移动、不删除文件。
- /import-local 不信任客户端报上来的路径:它重新扫描一遍 agent 目录,并要求请求里的路径确实出现在扫描结果里。否则这条路由就是一个任意文件读取原语。
- 导入是复制,不是把外部目录注册进来。就地注册的话,卸载本插件会连带删掉你的 Claude Code 配置,而且在 DSH 里编辑会改到别的 agent。
- 那张 35 行的目录表是只读的:它 list 一遍、读一遍 SKILL.md,从不往别家 agent 的目录里写任何东西。唯一的例外是 /import-local,而它写的是 ~/.dsh/skills,也就是你自己的 DSH 技能根。
插件的 exact/prefix 路由注册在裸 webServer 上,匹配优先于 DSH 自己的 /api 围栏,围栏看不到它们,所以同源校验必须自己做。
开发
node docs/smoke-host.mjs # 宿主半边:桩 cordis context + 真实 node:http + 真实上游
node docs/smoke-client.mjs # 客户端半边:迷你 React + 桩 DOM + 桩 fetch
node docs/check-classes.mjs # 样式表漂移:渲染出的 sc- 类是否都有定义
node docs/check-sorts.mjs # 各源的排序键是否真的排了序(需要联网)
node docs/preview.mjs # 把真实 bundle 渲染成 HTML,再用 Chrome 无头截图
node docs/preview.mjs --no-shot
node docs/audit-publish.mjs # 发布前自检:账号名残留与写死的绝对路径
node docs/check-published.mjs # 陌生人此刻在 GitHub 上看到的到底是什么(读 API)
匿名配额只有 60 次/小时;设 GITHUB_TOKEN 可提高退出码 0=全过 1=有失败 2=有项目没查成(网络/配额)
node docs/look-at-page.mjs # 上面那一页渲染出来好不好看(读 HTML,不是 API)
node docs/shot-page.mjs # 把那一页真的截一张图下来,自己看一眼
node docs/verify-clone.mjs # clone 一份公开仓库,验证陌生人的 checkout 真的能用
node docs/probe-npm-install.mjs # 从 npm 装进一个用完就删的 profile,验证陌生人那条路能用
(默认装 package.json 里钉住的那一版;给个 spec/路径可改)
node docs/probe-sources.mjs # 各上游可达性与契约实测
node docs/probe-validate.mjs # frontmatter 体检与改名的往返
node docs/probe-references.mjs # SKILL.md 引用扫描的误报/漏报
node docs/probe-agents.mjs # 其他 Agent 技能目录的发现结果(扫本机真实目录)
node docs/probe-roots.mjs # 那张目录表本身:每个 id 解析到哪个路径、覆盖变量改了谁(临时目录,不碰本机)
node docs/probe-tarball.mjs # tar.gz 解码器(合成包 + 一个真实仓库)
node docs/probe-freshness.mjs # 按 commit 钉住、整棵树比对与缓存命中(真实网络)
node docs/probe-glass.mjs # 抽屉在玻璃主题下还实不实、设置页里有没有多铺一张底板(先跑一次 preview.mjs)
node docs/probe-rail.mjs # 每个来源标签是否都在屏幕里(先跑一次 preview.mjs)
node docs/probe-toggle.mjs # 调用开关:翻过去是宿主认的那个值吗、翻回来是原来那份字节吗
node docs/probe-usage.mjs # 调用次数:多帧日志真的全读到了吗(合成日志,不读本机会话档)
两个冒烟测试都不碰真实的 ~/.dsh/skills(宿主测试写进 mkdtemp 临时目录)。
docs/preview.mjs 需要 Chrome 或 Edge;找不到时会打印一行并跳过截图(不失败)。
浏览器不在默认位置就用 SKILL_CENTER_CHROME=<可执行文件路径>。
docs/theme.css 和几份 slot / service 目录 JSON 是从 DeepSeek 自己的 bundle 里抽出来的, 没有提交进仓库(node docs/theme-tokens.mjs、node docs/extract-slot-catalog.mjs 可以对着你自己的安装重新生成)——所以全新 clone 直接跑 preview.mjs 之前,先跑一次 theme-tokens.mjs。
docs/verify-clone.mjs 就是把上面这件事反过来验一遍:它 clone 一份公开仓库到临时目录,跑一遍裸 clone 上应当通过的检查,确认没有哪个文件只存在于作者的机器上(生成的产物忘了提交、只在本机存在的路径、假设了兄弟目录的脚本)。它检验的是「别人拿到这个仓库能不能用」,不是「作者的机器上能不能用」。两件事不一样,这个脚本存在的唯一理由就是它们不一样。
(它自己踩过两个坑:spawnSync 的 cwd 指向尚未创建的目录会报 ENOENT,看起来像 "git 没装";把子进程输出接进管道需要一个具名管道,而沙箱会拒绝,失败以 result.error 上的 EPERM 抵达、stdout 为空——与"运行成功但没输出"无法区分。所以它用 stdio: 'inherit',并在 result.error 上显式报错。)
dsh.cmd 是一层两行的壳,真正的可执行文件是 Electron 自己(ELECTRON_RUN_AS_NODE=1 加一段 asar 里的 JS)。probe-npm-install.mjs 直接调那个二进制,不走 .cmd:Node 在没有 shell 的情况下拒绝 spawn .cmd(EINVAL),而走 shell 就要给一个含空格的路径加引号。它顺便回答一个发布前必须问的问题:宿主拿到的是一个插件,还是第二份它自己。后者正是 @deepseek-ai/dsh 写进 peerDependencies 的副作用,所以那一项标了 optional。
改动 lib/ 下的宿主代码后需要重启 harness 才生效;client/client.js 由 @deepseek-ai/dsh-client-hmr 轮询热重载,保存即可看到。
本仓库的脚本和文档含中文,不要用 PowerShell 的 Get-Content/Set-Content 做文本替换——Windows PowerShell 5.1 会按 ANSI 代码页往返,把多字节字符压成 U+FFFD。用编辑工具,或用 Node 读写 utf8。
预览回路
docs/preview.mjs 是改界面时的主要反馈回路:它在 Node 里求值一遍真实的 client/client.js,用桩 DOM / 桩 fetch 驱动真实交互(点侧边栏按钮开抽屉、点卡片进详情、点安装出确认面板),输出 10 个场景 × 明暗两套的 HTML 与 PNG,不重启 harness 就能看到界面。它顺带发现过两处夹具错误(counts 形状、/item 响应形状)、一处夹具缺口(/updates 少顶层 checkedAt,页脚渲染成 Invalid Date)与一处真 bug (完整性提示拼错了字段),值得在改渲染函数后先跑它。
这块面板不在玻璃主题下让路。 壁纸类插件把宿主变透明的办法,是把 --dsw-alias-bg-layer- 这几个别名重写成 color-mix(…, transparent);凡是拿这些别名当自己背景的面,壁纸就会透上来。对话气泡透一点没问题,一张罗列文件树的列表不行。所以面板的颜色画在一块不透明底板上:--sc-plate 取自静态调色板(--dsw-static-neutral-bluish-,任何主题都不改写它),--sc-solid 再把主题底色叠在它上面。没有玻璃主题时两层同色,等于什么都没改;有玻璃主题时面板仍是实心的。
但这张底板只铺在抽屉里,因为只有抽屉那块地是它的。设置页那一份是宿主的地盘,面板在那儿是客人。客人自己铺一张底板,就是在设置对话框上盖一个白方块,方到能从对话框自己的圆角里冒出来。0.2.1 就是这样,在设置页里看着像个异物。所以 --sc-solid 只画在抽屉上, .sc-root 什么都不画:抽屉里由抽屉铺,设置页里由宿主铺。
docs/probe-glass.mjs 把这个约定变成断言:它不问壁纸插件在不在,只问「别名被别人改成半透明之后,这块面还实不实」,并且先确认那次改写真的生效了,否则这个测试什么也没证明。同一个探针再从反面问一遍:设置页里 .sc-root 算出来的背景必须是 rgba(0, 0, 0, 0)。它顺手把两种渲染都拍下来(preview/glass-<theme>-before.png 与 glass-<theme>.png,一张是原来的样子、一张是现在的样子)。判断对错的不是这些图,是那十二个数;图是给会去看的人看的,免得他先把断言信了。
来源标签一行放不下就换行,不横滚。 原来是一行到底、放不下就左右滚,右边缘还加了一道渐隐遮罩,想让被切掉的标签看起来像「还有更多」。遮罩不是提示:一个来源可以整个落在边缘外面,而唯一能发现它的办法是去滚一条根本不画滚动条的带子。有人就是这么中招的——靠无意间按了一下方向键才看见「DSH 技能包」。现在 .sc-sources 会换行,flex-basis: 600px 是五个标签一行所需的宽度:要不到这么多,它就落到视图切换下面独占一行;要得到(设置页),它还在原处。docs/probe-rail.mjs 只问一个截图答不上来的问题:每个标签是不是都在自己的容器里。它问两遍,第二遍把旧声明强制加回去,因为一个不可能失败的检查不算检查。
文件
- lib/index.js 宿主入口:路由与分发
- lib/sources.js 五个来源适配器
- lib/github.js GitHub URL 解析 / 目录列举 / 文件收集
- lib/skills-dir.js 技能根扫描、安装、回收站、备份、改名
- lib/validate.js SKILL.md 体检与 name: 行改写
- lib/completeness.js 「这次取回的文件树是否完整」
- lib/references.js SKILL.md 引用的文件是否存在
- lib/tarball.js 手解 tar.gz(一次请求拿到一个 commit 的整棵树)
- lib/provenance.js 安装来路记录(<root>/.skill-center.json),含整棵树的哈希
- lib/updates.js 按来路记录比对上游:先比 commit,必要时再比整棵树
- lib/agents.js 其他 Agent 技能目录的发现与导入
- lib/usage.js 会话日志里数技能调用次数(自己按魔数切多帧 zstd)
- lib/frontmatter.js SKILL.md frontmatter 解析
- lib/http.js sendJson / readJsonBody / sameOrigin
- lib/net.js 带超时、重试、缓存的上游抓取
- client/client.js 浏览器 bundle(手写 classic script,无构建链)
- docs/mini-react.mjs 迷你 React + mount / renderHtml,冒烟测试与预览共用
兼容性
面向 DSH 0.2.0-rc.2。刻意不使用 @deepseek-ai/dsh-client-ui-primitives 的任何导出,也不 import installSettingsSection / settingsNamespace。前者在 0.1.7 会重命名图标,后者在当前版本根本不存在,而且缺失的具名导入是模块求值期的 SyntaxError,会整棵插件树一起崩掉。图标全部内联 SVG,颜色一律走 --dsw-alias- 令牌,因此自动跟随明暗主题。
不要按 peer 自身的版本号写 peerDependencies:DSH 的门禁是拿 range 去和 dsh 自己的版本做 semver.satisfies,所以 "@deepseek-ai/cordis": "~4.0.4" 这种写法语义是错的。
engines.node 写的是 >=20,但调用次数这一项要 Node 22.15 才有 zlib.zstdDecompressSync。低版本上插件照常工作,只是这一项会说明自己读不了,而不是显示一排 0。
想查某个 UI 插槽的契约:
node docs/extract-slot-catalog.mjs <你解包出来的 dsh-cordis-client-runner/lib/client.js>
node docs/show-slot.mjs sidebar.footer.action
样式表要自己认领,否则会被别人领走
宿主里只有两处代码会动 标签的归属,而它们都只看标签自己的属性:
- @deepseek-ai/dsh-client-modules 物化任何插件时,把它那一刻所有还没打 data-plugin 的 全部盖上那个插件的名字(claimStyles,lib/client.js:172)。
- @deepseek-ai/dsh-client-hmr 在某个插件热重载时,删掉所有 style[data-plugin=<它>](removeOwnedStyles,lib/client.js:54,80)。
这个 bundle 的样式表是在 apply() 里注入的,而 apply() 跑在自己的物化之后。那一刻 claimStyles 已经错过,标签是「无主」的。于是之后任何一个别的插件物化都会把它领走;用户下次更新那个插件时,宿主就顺手把技能中心的样式删掉了。DOM 还在、面板照常渲染,只是一条规则都不生效。看起来就是「界面乱了」,重启才好。
修法是照宿主自己的写法(@deepseek-ai/dsh-client-ui- 二十来个包全都这么干):
tag.dataset.plugin = 'dsh-skill-center' // 归属,claimStyles / removeOwnedStyles 用它比对
tag.dataset.pluginCss = 'dsh-skill-center/client.css' // 唯一标签 id,兼重复注入的守卫键
而且永不 remove:样式表属于文档,不属于某个 fiber,所以 disposer 是空函数。面板还挂着的把样式撤走,只会把它剥光。docs/smoke-client.mjs 的 [stylesheet ownership] 与 [the stylesheet survives a second activation] 两段就是守这三条,包括「再 apply() 一次不会多出一张样式表」。
视觉
视觉上贴着 DSH 自己的设计系统走,不带第二套配色。
DSH 基本是单色系统。--dsw-alias-brand-primary 浅色下是近黑 #0f1115、深色下是近白 #f9fafb,主按钮就是黑/白药丸;唯一成体系的彩色是 --dsw-alias-link(deepseek-500 / deepseek-400)。所以皮肤的强调色只有这一处蓝,状态色只用 red / green / amber 令牌。
--dsw-alias-bg-skeleton、--dsw-shadow-lv1/2/3、--dsw-font-、--dsw-alias-markdown-* 都直接复用,字号与圆角不自己发明。
浅色下 bg-base 与 bg-layer-1/2/3 四个值全是纯白,层次只能靠边框、遮罩与阴影做;深色下四层才真的分得开(950 / 875 / 850 / 800)。因此卡片用 border-l1 发丝边加 hover 才浮起,而不是给面板加底色。
还有一条是踩过才记住的:别名令牌会随主题翻转,底色不翻转的组件就不能拿它上墨。
--dsw-alias-toast-bg 在浅色和深色下都是深的(neutral-bluish-800 / 750),而 --dsw-alias-label-primary 浅色下是近黑,两个凑一起就是黑底黑字。这个 bug 出现过一次,只在浅色下可见,靠用户在真实界面里发现。给固定深色表面配字要用永远为浅的调色板项(--dsw-static-neutral-bluish-00),不是会翻转的别名;反过来,主按钮那种底色自己会翻转的地方(button-primary-fill + label-primary-foreground)就该成对用别名。
全部尺寸与颜色收敛在 .sc-scope 这一个作用域根的 CSS 变量里,主题切换不需要第二套样式表。
许可
MIT。插件本身不打包任何技能内容,安装时按需从上游取回;各技能的许可条款以它自己的仓库为准(预览页会显示识别到的 license 字段)。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
tuoLuoSuan/dsh-skill-center
DeepSeek Harness 的技能浏览器。安装前先做检查(名字不合法的会被 DSH 静默跳过),装下来的是某一次 commit 而不是分支。
相关插件 · 市场 / 管理
插件精选列表awesome-dsh-plugin/awesome-dsh-plugin
DeepSeek Harness(dsh)插件精选列表
★ 21,780未汉化DSH
73.5
新词雷达zhuyansen/new-term-radar
new-term-radar新词雷达
★ 1,947已汉化MCP
72.7
插件生态导航0xsline/awesome-deepseek-harness
安装 贡献指南 DeepSeek Docs 公开插件目录 Issues
★ 1,188已汉化DSH
72.6
技能注册中心iflytek/skillhub
企业级开源智能体技能注册中心——发布、发现并管理组织内可复用的技能包。
★ 5,347已汉化DSH
71.9
插件精选清单Anil-matcha/awesome-dsh-plugin
DeepSeek Harnessdsh精选指南——DeepSeek…
★ 1,013已汉化DSH
71.7
插件市场目录imsai-sh/awesome-deepseek-harness-plugins
面向 DeepSeek Harnessdsh生态的社区插件目录,共收录 13917 个插件含 PR 收录与…
★ 343已汉化DSH
71.6