写 DSH 技能
技能是给未来的自己(或姐妹)看的工作手册。写它只有两个标准:触发得准、照着做真的能做成。其余都是围绕这两条的工程细节。
本技能服务两种用法:在笔记本上写技能,在远程服务器上读技能干活。所以每一步都要能在「看不到界面、操作者不在现场」的条件下走完。
一、动手前先分清三件事
1.1 哪些技能是官方的(判据,别看 UI 标签)
技能页上的「来源:filesystem」是提供方名字,不是归属 —— dsh-skill-filesystem 这个提供方同时供给用户根和官方内置根,UI 区分不了。
真正的判据是两条:
| 判据 | 官方随包 | 用户自装 |
|---|---|---|
组合里有没有 bundledSkillDir / customSkillDirs |
有内置根才可能有 | 无 |
| 技能目录的真实路径 | DSH 安装树内(如 @deepseek-ai/dsh-agent-presets/presets/<preset>/skills/) |
$DSH_HOME/skills/ |
查法:
dsh --profile web --dump-default-config | Select-String -Pattern "skill-filesystem" -Context 0,6
Get-ChildItem "$env:DSH_HOME\skills" -Force # 注意 -Force,漏了隐藏目录会误判
这台机器的实情(2026-09-10 核):只有 cordis preset 随包发的 2 个技能(cordis-plugin-development、editing-cordis-compositions),经 preset 的 bundled 根(rank 600)进来。$DSH_HOME/skills 下那 20 个全是自装的。所以「DS 官方技能」目前只有 2 个 —— 想引用「官方做法」时,权威出处不是任何一个技能,而是 §二 的 README。
1.2 官方规范的权威出处
<DSH 安装树>/node_modules/@deepseek-ai/dsh-skill-filesystem/README.zh.md
<DSH 安装树>/node_modules/@deepseek-ai/dsh-skill/README.zh.md
规范以它们为准,不以任何技能为准 —— 包括本技能。本技能是规范的摘要 + 我们摔出来的经验;两者冲突时按 README 走,并把冲突记下来。
1.3 本技能与其它技能的分工
| 需求 | 用哪个 |
|---|---|
| 写/改技能、判篇幅、调 description | 本技能 |
| 诊断 DSH 起不来、技能加载失败、链接损坏 | dsh-repair |
| 写 DSH 的插件(不是技能) | dsh-plugin-development |
二、DSH 官方规范(摘自 dsh-skill-filesystem README)
2.1 文件格式与发现
技能是被扫描根目录顶层的目录 bundle <name>/SKILL.md,或平铺文件 <name>.md。刻意不支持发现嵌套的 **/SKILL.md —— 把技能埋进子目录等于没写。
2.2 frontmatter 字段
必填:name、description。
可选:whenToUse、metadata、disable-model-invocation、user-invocable。
| 字段 | 作用 |
|---|---|
disable-model-invocation: true |
把技能从面向模型的目录和 loader 中排除(人还能用,模型看不到) |
user-invocable: false |
从面向用户的命令中排除 |
| 省略 | 对应接口默认允许 |
这两个键接受 YAML 布尔值,以及不区分大小写的 true/false、yes/no、on/off、1/0。
被拒绝的拼写或非布尔值会让整个 skill 随警告一起被丢弃 —— 而不是静默允许某个接口。 所以 disable-model-invocation: off 是合法的,disable-model-invocation: nope 是致命的。
2.3 目录与正文有独立的生命周期
发现阶段把 frontmatter 解析进目录条目;每次加载都重新读取当前文件。所以改正文不需要版本化或缓存失效 —— 存盘即生效(下一个模型步骤)。
2.4 根与优先级
按 rank 顺序扫描,数字小的先命中:
| Rank | 来源 | 路径 |
|---|---|---|
| 100 | project-dsh |
<projectRoot>/.dsh/skills |
| 200 | project-agents |
<projectRoot>/.agents/skills |
| 300 | custom |
Config.customSkillDirs |
| 400 | user-dsh |
<dshHome>/skills |
| 500 | user-agents |
<agentsHome>/skills |
| 600 | bundled | 配置了 bundledSkillDir 才有 |
项目根是包含 .git 的最近祖先目录;不存在则用当前 cwd。用户 DSH 根会跳过其 .system 子目录(那是预留区)。
实践含义:同名技能可以靠 rank 覆盖 —— 项目级(100)压过用户级(400)。要在某个项目里临时改掉某个技能的行为,放一份到 <projectRoot>/.dsh/skills/<同名>/ 即可。
2.5 变更检测
现有根被监视。新增、改名、删除技能,或编辑它的 frontmatter,都会在下一个模型步骤触发目录刷新。
references/、scripts/、assets/ 等 bundle 资源下的编辑不触发刷新(因为正文每次加载都重读,资源本来就该在用时才读)。
第一方的 write 与 edit 工具命中受监视的技能时会直接使提供方失效,所以模型不必等宿主 watcher 就能看到自己的改动。外部 IDE / Git / shell 的改动由宿主 watcher 捕获。
2.6 失败长什么样(最容易被骗的一节)
缺少有效 frontmatter、名称无效或调用值无效的文件会随警告被跳过,因此模型目录不会收到逐 skill 诊断,也无法区分缺失的 skill 与无效的 skill。
这句话是本节最贵的知识。它意味着:技能"不见了",你无法从模型侧判断它是被删了、路径变了、还是 frontmatter 写坏了。 唯一可靠的排障手段是 §四 的静态校验脚本 —— 别靠"重启试试"。
2.7 YAML 标量的两个静默杀手:: 与 #
frontmatter 是未加引号的 YAML 标量。两个元字符会出事,症状完全不同,所以别只防一个:
| 字符形态 | 机制 | 症状 |
|---|---|---|
: (半角冒号 + 空格) |
开启嵌套映射 → 解析失败 | 技能从目录里消失,没有任何报错(§六 事故一,2026-09-10 实测) |
#(前面有空格的 #) |
被当注释起始符,后半句整段吞掉 | 技能还在、照常加载,但字段被悄悄截断 —— 如 React #31 之后的内容全丢(§六 事故三,2026-09-17 实测) |
# 这条更阴:解析成功、技能照常进目录,只是触发面残缺,页面和日志里都没有任何提示。YAML 规范要求注释由空白之后的 # 引出,所以 React#31(# 前无空格)不会被吞 —— 2026-09-17 在本机 yaml 包上实测确认。
修法:长文本(description、whenToUse)要么整个加引号 —— 单引号最省事,'…React #31…' 实测能完整保留 —— 要么避开 : 与 # 这两个形态。更省心的规矩是:写 frontmatter 时默认 YAML 会吃字符,改完必须解析并断言字段完整(§4.1),不靠肉眼。
三、黄金铁律
- description 里既不能出现
:(半角冒号+空格),也要当心#。 frontmatter 是未加引号的 YAML 标量:link: xxx让解析失败 → 技能静默消失;React #31这种#前带空格的写法则是解析通过、后半句被当注释吞掉(§2.7)。要么用全角「:」/避开这两个形态,要么给整个值加引号。改完必须解析并断言(§4.1)。 - 改 frontmatter 前先备份整个技能目录。 frontmatter 是"热"的(§2.5),改坏了立即从目录里消失;没有备份就只能重写。
name一旦定下就别改。 改名等于换了个技能:旧名从目录消失、用户调用路径失效、别的技能里对它的引用全部悬空。- 判定"技能装了却不出现"先跑静态校验,不要先怀疑 DSH。 §2.6 说了模型侧拿不到诊断。
- description 是唯一的触发机制。 正文写得再好,description 不含触发线索就等于没这个技能。系统提示里只有 name + description 进模型视野。
- 技能必须写过测试才算写完。 见 §五 —— 只读一遍觉得没问题不算验证。
- 篇幅控制在 300–500 行(一条实践约定,2026-09-10 定的基线)。低于 300 行往往漏了边界情况;超过 500 行说明该拆
references/了。 - 派子 agent 做技能测试/审计时,把扫描范围写成白名单。 明写「禁止递归
node_modules/.pnpm/ lockfile」,否则它会扎进依赖树烧掉几百万 token(见 §六 事故二)。 - 只改私有原件(
$DSH_HOME/skills/<name>/),不拿发布副本当工作区。 发布副本是脱敏后的产物:它不在任何技能扫描根里(§2.4),改它既不影响本机行为、也不会上线。详见 §九。 - 同步副本进公开发布物之前,必须跑一遍禁用词扫描。 2026-09-13 实测出过真泄漏 —— 已发布的公开产物里带着本机绝对路径、真人名、内部称呼(§九)。
四、静态校验(写完立刻跑)
本技能自带 scripts/check-skill.js,一次查五件事:
node <本技能目录>/scripts/check-skill.js "$DSH_HOME/skills/<技能名>"
它检查:
- frontmatter 能否被 YAML 解析(事故一的直接产物)—— ⚠️ 它只拦得住「解析失败」和
:;#那类"解析成功但字段被吞"的静默截断它查不出来(§2.7),得靠 4.1 单独断言 - 必填字段在不在、
name是否与目录名一致、布尔字段是否合法拼写 - 正文里引用的
references/scripts/文件是否真实存在 - 站内锚点
](#xxx)与正文里的§N是否指向真实标题(标题改了忘改链接是常见腐烂)。⚠️§N只在本文件里找——所以「见 REFERENCE§4」这种跨文件引用会被误报成「交叉引用找不到对应章节标题」(2026-09-12 在dsh-plugin-development上实测)。要指别的文件的章节,就写章节全名或文件名,别用§N;实在要举例,把§N放进行内代码里(脚本会剔除代码再扫描)。代码块内的§N同样不检查。 - 行数是否落在 300–500 的建议区间
退出码非 0 就是有问题。加 --json 可给自动化流程吃结果。
为什么必须静态校验而不是"重启看看":§2.6 —— 无效技能被跳过时模型侧无法与"缺失"区分,重启只会让你看到同样的"不见了"。
4.1 断言 frontmatter 字段完整(补 # 这类静默截断)
解析成功 ≠ 字段完整。改完 frontmatter 后跑一遍(2026-09-17 实测,本机 yaml 包):
node -e "const fs=require('fs'),y=require(process.env.DSH_HOME+'/profiles/node_modules/yaml');const fm=fs.readFileSync(process.argv[1],'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)[1];const m=y.parse(fm);for(const k of ['name','description'])if(!m[k])throw new Error('缺 '+k);for(const l of fm.split(/\r?\n/)){if(/^[A-Za-z0-9_.-]+:/.test(l)&&/ #/.test(l))throw new Error('未加引号的值里有 # 会被当注释吞掉: '+l)}console.log('frontmatter OK: '+Object.keys(m).join(', '));" "<技能目录>\SKILL.md"
它做三件事:真 YAML 解析、必填字段非空、逐行检查未加引号的值里有没有 #(注释起始符的形态)。退出码非 0 就是有截断。
yaml 包的位置随 DSH 安装树变:check-skill.js 的 loadYaml() 里那串候选路径是权威写法,换机器时照它改。
五、验证技能真的管用
静态校验只证明"文件合法",不证明"照着做能做成"。后者必须做行为验证。
5.1 A/B 对照(唯一诚实的做法)
技能的价值 = 带它与不带它做同一件事的差距。所以:
- 写 2–3 个真实的、具体的测试 prompt(不是"格式化数据"这种抽象话,要带路径、背景、口语化措辞)。
- 同一轮派两组子 agent —— 一组带着技能路径,一组不带(或带旧版快照)。同轮派发,别先后。
- 对比产物,而不是对比它们的自我报告。
- 空结果、跑偏、只做对一半,都算不过;据此改技能再跑下一轮。
技能的基线随类型不同:新建技能用"完全不给技能";改现有技能用"改之前那份快照"(先 robocopy 一份出来当基线)。
5.2 派子 agent 的纪律
- 给每个子 agent写死:任务边界、扫描白名单、交付格式、产物落盘路径。
- 明写「禁止递归
node_modules/.pnpm/pnpm-lock.yaml/ 打包产物」。 - 要它带证据回报(
文件:行号或命令输出),没取到证据的必须标「未取证」。 - 子 agent 会盲信自己写的总结 —— 交付前抽查它的证据链,别 blind-trust。
5.3 什么时候可以不跑 A/B
只改错别字、只改注释、纯排版调整,跑静态校验就够。只要改了 description 或正文的判定逻辑,就得跑。
六、真实事故
本节的复盘按 §7.6 的约定处理:结论留在这里,长到会稀释主流程的完整复盘才另建 references/case-YYYYMMDD.md。
事故一 · frontmatter 里的一个冒号,让技能消失(2026-09-10)
给 dsh-repair 扩写 description 时写进了 mounted with link: that cannot reach...。半角冒号+空格在未加引号的 YAML 里开启了一个嵌套映射 → 解析失败 → 技能立刻从技能目录里消失,没有任何报错指向原因。
修法是删掉那个冒号;watcher 热加载让它自动回来。
教训:改 frontmatter 前后各跑一次 check-skill.js;改前先备份。
事故二 · 派子 agent 审计,烧掉 5.5M token(2026-09-10)
派子 agent 审计五个插件对 DSH 新版兼容性,提示词只写了「下载包对照 + grep 插件源码」,没锁范围。它扎进 .pnpm 私有副本和 131 KB 的 lockfile,烧了 5.5M token 还没交报告,最后被中止。那份审计换成定点 grep 十分钟就做完了。
教训:§5.2 的纪律不是形式主义 —— 扫描范围要写成白名单,不是描述意图。
事故三 · # 吞掉 description 的后半句(2026-09-17)
复核 frontmatter 时发现某个技能的 description 只剩半句,原文里有 React #31:其中 # 被 YAML 当成注释起始符,#31 之后的内容全部丢失(同一机制对记忆文件的 frontmatter 一样成立 —— 都是 YAML)。技能没有消失、解析没有报错,所以此前所有"看一眼 frontmatter、觉得没问题"的检查都放过了它。
教训:
- 「解析成功」不等于「字段完整」——
#类截断只有断言(§4.1)看得见,check-skill.js也拦不住。 - 这类残缺只损失触发面、不损失功能,所以永远不会有人报告它坏了;只能在发布与复核时主动查。
- 同一个坑半年内踩两次(09-10 冒号、09-17 井号)说明:改 frontmatter 的收尾动作必须是跑命令,不是"再看一眼"。
七、怎么写:流程
7.1 先问清需求
- 这个技能覆盖什么任务/领域?
- 要处理哪些具体场景?边界在哪(哪些情况不该触发)?
- 需不需要可执行脚本,还是纯说明就够?
- 有没有现成参考资料要打包进去?
需求稀疏时先问,别猜着写 —— 猜出来的技能会写得太宽(什么都想覆盖)或太窄(只覆盖你想到的那一个例子)。
7.2 定触发面(description)
description 进模型视野,正文不进。它的任务只有一个:让模型在正确的时刻想起这个技能。
系统提示里技能容易触发不足(该用时不用),所以 description 要稍微推一把:
- 第一句说清"这个技能能干什么"。
- 之后堆具体触发线索:用户会说的原话、文件类型、路径片段、报错片段、界面线索。
- 明确包含"即使对方没说出 X 这个词"这类兜底。
- 有近似但该走别的技能的情况时,把边界也写进去。
好例子:
为 DSH 编写、修改和验证技能(SKILL.md)。…… 无论用户是要新建技能、改正文或 frontmatter、
诊断「技能装了却不出现」、还是问该写多长、该不该拆文件,都要用本技能 —— 即使对方没说出 skill 这个词。
坏例子:
帮助处理技能相关的事情。
坏例子里没有任何可区分的线索,模型无法把它和别的技能分开。
7.3 写正文
按「先结论、再依据、最后细节」组织:
- 一段话定位 —— 这个技能解决什么问题、服务谁、在什么前提下用。
- 判据与规范 —— 硬事实(字段、优先级、限制),标明出处。
- 流程 —— 可直接照做的步骤,每步带判读标准。
- 边界与反例 —— 什么时候不该用、容易踩的坑。
- 验证 —— 怎么证明做对了。
写作要点:
- 用祈使句,直接说要做什么。
- 解释为什么,别只下命令。模型判断边界靠的是理解意图,不是背规则;满屏 MUST/NEVER 反而让它僵化。
- 别写时效性内容("当前最新版是 0.1.5" 半年后就是错的)—— 写成"怎么查当前版本"。
- 术语前后一致,同一个东西别换三种叫法。
7.4 决定要不要 scripts / references
加脚本,当操作是确定性的(校验、格式化)、同一段代码会被反复现场重写、或需要显式错误处理。脚本省 token 也比现场生成的代码可靠 —— 而且能被执行而不必读进上下文。
拆 references/,当:
- SKILL.md 超过 500 行;
- 内容有彼此独立的领域(比如"Windows 做法"和"Linux 做法");
- 有很少用到的高级内容(放正文会稀释主流程)。
SKILL.md 只留导航 + 主流程,细节下放到 references 并从正文指过去,说明什么时候该去读它。大参考文件(300 行以上)开头放目录。
7.5 骨架模板
---
name: kebab-case-名字
description: <做什么>。Use when <具体触发线索、原话、文件类型、报错片段>,即使对方没说出 X 这个词。
whenToUse: <可选,一句话场景>
---
# 标题
一段话定位:解决什么问题、服务谁、什么前提下用。
## 判据 / 规范
硬事实,标明出处。
## 流程
1. 步骤 —— 带判读标准
2. 步骤
## 边界与坑
什么时候不该用;已知会踩的坑。
## 验证
怎么证明做对了。
## 详见
- [references/细节.md](references/细节.md) —— 什么时候读它
7.6 把事故沉淀进正文(别只留在会话记忆里)
会话记忆会消散,技能正文不会 —— 这是技能最被低估的用途:它是上一个自己给下一个自己留的教训。写不写只有一条判据:
| 内容 | 处置 |
|---|---|
| 能靠读代码 / git / 配置文件推出来的 | 不写 —— 写进去只是会腐烂的冗余 |
| 踩过一次坑才知道的:机制、症状、误判路径、修法 | 写,并写清"怎么发现的" |
四条格式要求:
每条带日期,并标 实测 或 未验证。只有真跑过、有命令输出或产物为证的才算实测;推论、文档抄来的、听来的都标未验证 —— 别让未验证的经验穿上实测的外衣。
日期是出处,不是时效断言。 「2026-09-17 实测」不会过期;「当前最新版是 0.1.5」半年后就错了(§7.3)。这两件事不矛盾,别拿一条否定另一条。
症状 → 嫌疑层 的速查表比长篇叙述好用。 复盘写得再细,真出事时人只会问"现在先查哪"。本技能域内的速查表:
症状 先查哪里 技能从目录里消失 frontmatter 能否解析( :,§2.7)→ 跑check-skill.js技能在,但从不触发 description 是否被 #截断(§2.7)→ 触发线索是否太泛(§7.2)正文改了没生效 是不是改到了发布副本而不是私有原件(§九) 公开产物里有本机路径 / 人名 同步前漏跑禁用词扫描(§九) 发布版行为和本机不一样 打包清单是否多带了文件(§九) 长复盘下放
references/case-YYYYMMDD.md。 SKILL.md 里只留一两行指针 + 最关键结论(§六 就是这么处理的:三起事故各留结论)。什么时候拆,判据同 §7.4 —— 内容长到会稀释主流程才拆,别为归档而拆。
沉淀的时机是事故当场,不是"回头补"。回头补的代价是重演一遍排查。
八、篇幅与拆分
| 体量 | 处置 |
|---|---|
| < 100 行 | 可能太薄,先想清楚有没有漏掉边界情况 |
| 300–500 行 | 目标区间(一条实践约定,2026-09-10 定) |
| > 500 行 | 拆 references/,正文留导航与主流程 |
不要为了数字硬拆。 拆分的判据是"内容有没有独立领域",不是行数本身 —— 把一个连贯的流程砍成两半扔进两个文件,比长一点更糟。references/ 下的文件不计入正文篇幅,可以放开写。
九、发布:私有原件与发布副本
我们的技能有两份,职责不能混(2026-09-13 定的规矩):
| 路径 | 用途 | |
|---|---|---|
| 私有原件 | $DSH_HOME/skills/<name>/ |
日常使用与修改的唯一场所(技能扫描根,rank 400,§2.4) |
| 发布副本 | <workspace>/publish/ |
脱敏后发到 GitHub / npm 的那一份,是产物不是工作区 |
9.1 两条硬规矩
- 平时只改私有原件;要发布时再生成脱敏副本。 发布副本不在任何技能扫描根里(2026-09-17 核本机
--dump-config里没有customSkillDirs,查法见 §1.1),所以改它既不影响本机行为、也不会自动上线 —— 在它上面改东西是最容易白干的一种错。 - 同步副本进公开发布物之前,必须跑一遍禁用词扫描。 这条是拿真事故换来的,见 9.2。
9.2 事故:公开产物的 0.1.1 / 0.1.2 里带着内部信息(2026-09-13 实测)
复核发布产物时发现,某个技能已发布的 0.1.1 / 0.1.2 里带着这些内容:
- 本机绝对路径:插件源码目录、profile 的
package.json路径 - 真人名
- 内部称呼
它们在私有原件里完全正常 —— 本来就是自己用的东西 —— 但公开出去不合适。具体字符串不写进这里:本技能正文自己也会被发布,把泄漏内容抄进来等于二次泄漏。词表放在发布树之外的词表文件里。
扫描做法:
# 词表放发布树之外(如 $DSH_HOME\publish-scan-words.txt,按需自建、一行一个词);
# 词表放进发布树等于它自己先泄漏
$words = Get-Content "$env:DSH_HOME\publish-scan-words.txt"
Get-ChildItem -Path <发布副本目录> -Recurse -File |
Where-Object { $_.Extension -in '.md','.yml','.yaml','.json','.js','.ts' } |
Select-String -Pattern $words
管道机制 2026-09-17 在本机 PowerShell 上实测可用(只读扫目录);按发布物类型补词表才是真正花时间的部分。
命中不等于必须删。 判断标准是"公开后有没有危害",不是"命中了没有":说明性的示例路径可以留(本技能正文自己就有 $DSH_HOME\... 这类路径,写成真实盘符时扫描才会命中),机密的、能定位到人的、能暴露内部结构的必须改写或删掉。逐条人工过,别阈值化。
扫完再用 npm pack --dry-run 核一遍打包清单(9.3)。
9.3 发布形制:携带技能的插件
技能自己不是 npm 包。我们的发法是「携带技能的插件」:cordis.patch.yml + skills/,不带 lib 代码。已有三个技能是这样发出去的:dsh-repair、dsh-skill-authoring、dsh-subagent-delegation。
打包内容要过两道闸:
package.json的files白名单 —— 只列扩展自有文件(cordis.patch.yml、skills/、README、LICENSE)。npm pack --dry-run核对真实打包清单 —— 别信files写了什么,信npm输出什么。多出一个文件就多一份泄漏面。
原则一句话:只带扩展自有文件,别的私人定制(其它技能、profile 配置、memory)一律不进去。
十、交付前自检清单
-
check-skill.js退出码 0(frontmatter / 引用 / 锚点 / 篇幅全过) - frontmatter 跑过完整性断言(§4.1),而不只是"解析没报错"
-
name与目录名一致,且没打算再改 - description 有具体触发线索,且不含
: - 正文用祈使句,关键处解释了为什么
- 没有时效性断言(「当前最新版是 0.1.5」这类;经验条目的出处日期不算,见 §7.6)
- 术语前后一致
- 超过 500 行已拆 references,且正文里有指向它们的入口
- 改了 description 或判定逻辑 → 跑过 A/B 对照,且知道差距在哪
- 整个技能目录已备份(尤其动过 frontmatter 之后)
- 有真实例子(具体到路径、命令、判读标准),不是抽象描述
- 只改了私有原件;发布副本是按 §九 的流程重新生成的,不是在副本上直接改的
- 要发布 → 已跑禁用词扫描、命中项逐条判断过,
npm pack --dry-run核对过打包清单(§九) - 新踩的坑当天写进正文(§7.6),带日期与「实测 / 未验证」
十一、相关
- 官方规范原文 →
<DSH 安装树>/node_modules/@deepseek-ai/dsh-skill-filesystem/README.zh.md - DSH 起不来 / 技能加载失败 / 链接损坏 →
dsh-repair技能 - 写 DSH 插件(宿主插件、界面插件)→
dsh-plugin-development技能 - 本技能的静态校验工具 →
scripts/check-skill.js - 旧的重度评测工具 → 原
skill-creator技能(Anthropic 出品,Python 基准脚本 + eval viewer + 盲比流程,18 个文件)。2026-09-10 与write-a-skill一起归档在<archive>/skill-merge-20260910/,从技能目录移除是为了消掉三者的触发面冲突。需要跑量化基准或盲比时再去那里取;日常按 §五 做 A/B 对照就够。