Passer au contenu principal
T

dsh-skill-center

tuoluosuan/dsh-skill-center

Browse, preview and install public Agent Skills from inside the DSH web GUI, aggregating five sources (claudeskills.info, skillsmp.com, the awesome-claude-skills repository list, the official anthropics/skills repo, and skill packs shipped by installed DSH plugins). Skills install pinned to a commit rather than a branch name, so what you previewed is what lands; the panel reports provenance, whole-tree update checks and a trash instead of deleting. Before writing anything it validates the skill name and frontmatter against the rules the harness silently enforces, lists files that SKILL.md references but the fetch did not bring, flags partial fetches, and offers overwrite, rename or skip when a name is taken.

Installer

dsh plugin --profile web add github:tuoluosuan/dsh-skill-center

README

技能中心 · 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](references/schema.md) 指向的文件如果不在取回的目录里,会被列出来。这类问题只在模型第一次真用它时才爆,而且爆在别人的任务中间
同名冲突目标名字已被占用时不静默覆盖,也不静默加 -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

样式表要自己认领,否则会被别人领走

宿主里只有两处代码会动 <style> 标签的归属,而它们都只看标签自己的属性:

  • @deepseek-ai/dsh-client-modules 物化任何插件时,把它那一刻所有还没打 data-plugin 的 <style> 全部盖上那个插件的名字(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 字段)。

Plugins associés