1.4 卸载、更新与重名裁决
技能的生命周期管理
技能是文件,所以管理手段都是文件操作加组合操作。把常见动作列全:
| 动作 | 本地技能 | bundled 技能 |
|---|---|---|
| 安装 | 复制 / 克隆到技能根 | 在组合里加提供方行 |
| 更新 | 覆盖文件或 git pull |
升级 dsh 版本 |
| 临时停用 | 改名加 .off 后缀 |
摘掉配置行 |
| 彻底卸载 | 删除目录 | 摘掉配置行 |
| 查看来源 | 目录里就有 | ctx.skills 注册表摘要 |
| 验证 | 列表查看 / 金丝雀技能 | 挂载后列表查看 |
| 换层迁移 | 复制到目标层,旧层加 .off,验证后删除 |
调整组合配置行 |
| 审计 | 读摘要的 source / provider / path |
同左 |
“临时停用”这个技巧值得单独说:把目录名改成 my-skill.off,名称立刻不合 kebab-case 规范,技能从目录消失,但文件还在。想恢复就把后缀去掉。比删了再找回来稳妥得多。它同时是个诊断手段——技能突然消失又回来,能帮你确认问题到底出在技能本身还是别处。
换层迁移:从用户级搬到项目级
技能放错层是高频问题(比如先在用户级试了一下,后来决定随项目走)。标准动作四步:
- 把目录复制到目标层(如
.dsh/skills/); - 给旧层目录加
.off后缀停用,先别删; - 读一次摘要,确认
source已变成目标层、path指向新位置; - 跑一次真实任务确认行为不变,然后删除旧层目录。
顺序不能反:先停后删、先验证后清理,任何一步没达到判据就停在上一步。
更新的三种节奏
本地技能的更新分三种节奏,按来源选:
| 来源 | 更新节奏 | 操作 |
|---|---|---|
| 手工维护的个人技能 | 想到就改 | 直接编辑,watcher 实时生效 |
| 克隆来的技能仓库 | 跟随上游 | git pull --ff-only |
| 项目内团队技能 | 随 PR 走 | review 后合并,clone 即得 |
第三种最容易被忽略:团队技能也是普通文件,它的“发布”就是一次代码合并。这意味着技能变更可以、也应该走和代码变更同样的评审流程——description 改坏了,影响面比一个函数改坏了大得多,因为它直接决定模型还认不认这个技能。
重名裁决的完整规则
装了十几个技能后重名几乎必然发生。官方规则可以浓缩成两句话:
第一句:跨层看 scope 链,近者胜。 读取时把全局层与观察 scope 的链合并,最近层的条目直接赢得重名 skill。
~/.dsh/skills/code-review/ (rank 400,用户全局)
./.dsh/skills/code-review/ (rank 100,项目级)
→ 在仓库内工作时,项目级胜出
→ 在仓库外工作时,用户级生效
第二句:同层内比三样,顺序固定。 rank → 提供方顺序 → 本地顺序;摘要按名称排序。更细的层级结构与三个实例见 1.2。
这套规则的价值在于有意的分层覆盖:全局放通用版,项目放定制版,不需要任何条件判断语法。
一个反直觉的点:近者胜是“覆盖”而不是“合并”。项目级技能不会继承用户级技能的内容,模型只看到胜出的那一份。想让定制版包含通用版的全部指令,就把通用内容复制进去,或者把共享材料放进 references/、两个版本都引用它。
提供方级别的控制:signal 与 invalidate()
除了单技能变更,整个提供方目录也可能需要刷新。提供方工厂在注册作用域内收到两种控制能力,语义不同,别混用:
| 能力 | 语义 |
|---|---|
signal |
注册失败或该提供方被 dispose 时中止。它是止损,不是刷新 |
invalidate() |
仅在精确注册仍活动时清除已完成目录。它要求注册仍然有效,乱序调用不生效 |
一个是“出事时叫停”,一个是“还有效时清场”。区分这两者的意义在于:invalidate 不是万能刷新按钮,注册已经不准确时它什么都不做。
对使用者,这两种能力多数时候是隐形的,但在这些场景会派上用场:
| 场景 | 会发生什么 |
|---|---|
| 提供方注册失败 | signal 中止该提供方,其他提供方不受影响 |
| 提供方被 dispose(摘配置行、卸载插件) | signal 中止,其技能从后续读取中消失 |
| 需要强制刷新已完成目录 | 注册仍准确时 invalidate() 清场,下次读取重新发现 |
提供方代次变化时会发生什么
发现过程中提供方代次可能变化(远程提供方重连、目录被替换等)。官方的处理策略分两步:
- 重试一次——多数抖动一次就能过去;
- 再次变化——返回最新候选,标记该次观测为不完整,并且不缓存。
与之相关的还有两条观测规则:显式请求的不完整观测同样提供可用候选但不缓存;格式错误的候选则快速失败,不会静默跳过。
把观测的三条规则放在一起记:
| 规则 | 行为 |
|---|---|
提供方 list() 被拒 |
记录日志,从该次观测中省略其结果 |
| 显式的不完整观测 | 提供可用候选,但不缓存 |
| 格式错误的候选 | 快速失败,不静默跳过 |
对使用者的含义很直接:你不需要手动清缓存。遇到一次不完整的观测,重新读取一次通常就能拿到完整结果。
另外,发现缓存以解析后的 scope 链为键:重设 scope 父级(如空会话重组)无需注册表变更,即可被下一次读取看到——换 scope 不用重启。
怎么查“现在生效的是哪一个”
注册表返回的摘要带有来源与提供方字段,每个技能能看到:
| 字段 | 用途 |
|---|---|
source |
来源桶,如 project-dsh、user-dsh、bundled |
provider |
拥有该技能正文的提供方名 |
path |
指令文件的绝对路径(本地技能有,虚拟技能无) |
invocation |
模型 / 用户两个调用开关 |
实操方法:对可疑技能读一次摘要,看 path 指向哪——生效的就是哪份文件;source 告诉你它从哪一层来,provider 告诉你由谁供给。三者对不上,就是重名裁决或目录层级出了偏差。这是排查“我改了却没生效”最快的办法。
举个完整例子。你在用户级和项目级各放了一份 code-review,不确定仓库内生效的是哪份:
- 在仓库内会话读取技能摘要,找到
code-review条目; - 看
source:显示project-dsh说明项目级胜出,符合跨层近者胜的规则; - 看
path:指向.dsh/skills/code-review/SKILL.md,打开它确认是你改的那份; - 若
path指向用户级,说明项目根判断和你想的不一样——当前目录可能不在含.git的仓库内。
四步之内一定能定位,不需要读源码。
「装了没反应」六项排查清单
按命中率从高到低,每项都给了判据:
- 放错目录层。 判据:摘要里的
source是否等于你期望的那一层;在项目内还要确认项目根真的是含.git的目录,而不是它的某个子目录。 - 名称不合规。 判据:对照
^[a-z0-9]+(?:-[a-z0-9]+)*$——大写、下划线、点号都会让技能直接不出现。 description没写触发条件。 判据:拿你的提问和 description 对一遍,看触发场景是否写在明面上。这是“装了没反应”的头号原因。- 改了没保存 / 保存到了副本。 判据:打开
path指向的文件,确认内容是你刚改的版本。平白无故一类,但确实高频。 - 期望递归发现。 判据:技能是否平铺在技能根直属;嵌套子目录里的
SKILL.md不会被发现。 - watcher 观测不完整。 判据:重载组合或重启后是否恢复。极少数情况,且不会隐藏本来可读的候选。
症状与第一嫌疑对照,排查时先看这张表:
| 症状 | 第一嫌疑 |
|---|---|
| 目录里根本没有 | 第 1、2 项 |
| 目录里有,模型不选 | 第 3 项 |
| 改了内容不生效 | 第 4、6 项 |
| 嵌套目录里的技能不出现 | 第 5 项 |
一个务实的管理建议
技能数量上去以后,建议在仓库里维护一份清单文件,例如 .dsh/skills/README.md:
# 本项目技能清单
| 技能 | 来源 | 负责人 | 用途 |
|---|---|---|---|
| code-review | 本仓库 | @张三 | 按公司规范评审 PR |
| migrate-runbook | 本仓库 | @李四 | 数据库迁移六步法 |
技能是纯文本,最大的风险不是坏掉,而是没人知道它为什么存在。一份清单能让技能从“个人技巧”变成“团队基础设施”。
清单要随技能一起演进:新增技能时加一行,停用技能时标注而不是删行,每隔一段集中过一次,把确认没人用的技能清掉。技能目录会撒谎——文件还在不代表还有人用,清单至少能逼出一次集体确认。
小结
- 停用优先用改名加后缀,别急着删。
- 重名两规则:跨层近者胜,同层比 rank → 提供方顺序 → 本地顺序。
signal管中止,invalidate()管清目录;代次连变两次会得到不完整且不缓存的观测。- 用摘要里的
source/provider/path查生效来源。 - “装了没反应”先查
description,再看path。 - 给项目技能配一份清单,别让技能变成孤儿。
延伸阅读
- 官方子系统参考:skill 子系统
- 本书后续:第二章 官方自带:开箱即用的 Office 技能族
- 相关机制:DeepSeek Harness 组合包与 patch 层叠
