卸载 dsh plugin 的顺序有讲究:DeepSeek Harness 先插件后核心包与依赖清理
卸载 dsh plugin 讲顺序,是因为 profile 里有两份清单——dependencies 记录包、dsh.profile.bundles 记录加载层——只要其中一份先被清掉,profile 就会照着另一份去找一个不存在的包,启动时报 plugin tree failed to load。 所以正确次序是固定的:先停进程 → 先卸上层插件 → 再动共享依赖与核心包 → 最后才清 profile 目录与缓存。顺序对,每一步都干净;顺序反,就要回头排雷。
为什么要讲顺序:先卸 dsh plugin,再动核心包与依赖
这条次序不是洁癖,而是由「两份清单 + 一层保护」决定的(来源)。 三点原因:
- 两份清单必须同进同出:
dsh plugin remove会同时清掉 pnpm 依赖与dsh.profile.bundles那一项加载层;而手删node_modules目录只动文件,两份清单原样保留,profile 仍会按清单去解析一个已经不在的包。先删文件、后补清单,就是最典型的错误顺序。 - 共享依赖被别的插件引用时删不掉:pnpm 不允许把仍被其他依赖引用的包直接移除。如果先动底座包,要么被拒绝,要么留下一个引用了缺失包的插件——两种情况都得回头收拾。
- 进程正在加载插件时动文件会留半截状态:插件被加载期间删除它的文件,容易留下解析到一半的现场。第一步永远是先退出正在跑的
dsh web或桌面端。
一句话:顺序的目标是「让 profile 的每一份清单在每一步都指向真实存在的包」。
三种情形的正确顺序:卸单个 dsh plugin、卸一批、卸到只剩官方组合包
三种情形的共同骨架是「停进程 → 记现场 → 从上往下卸 → 验证」,区别只在于中间那一步清多少。 分别如下:
情形一:卸单个插件
# 1. 退出正在运行的 dsh web / 桌面端(Ctrl+C)
# 2. 抄准包名,别凭记忆
dsh plugin --profile web list
# 3. 卸载(rm / uninstall / un 等价)
dsh plugin --profile web remove <包名>
# 4. 验证两处(见下一节)
情形二:卸一批插件
顺序原则是先卸依赖别人的,再卸被别人依赖的:先清上层插件,pnpm 眼中的依赖关系会逐层解开,轮到底座包时它已经不再被引用,可以干净移除。也可以一次传多个包名(dsh plugin --profile web remove <包名A> <包名B>),但这不改变先上层后底座的道理——最省事的做法是一个一个来,每卸一个验证一次。
在插件市场里做这件事有可视化路径:打开「设置 → 插件市场」即 DSH Plugin Hub,切到已安装插件列表,找到目标插件点行尾的「卸载」,确认弹窗里会列出插件与来源仓库。逐项移除天然就是从上往下,也不容易漏掉清单里的一项。可视化的完整步骤见 用 DSH Plugin Hub 卸载插件。
情形三:卸到只剩官方组合包
只卸 out-of-tree 的插件,内置组合包一个都别动——@deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app、@deepseek-ai/dsh-headless 从 dsh 安装本体解析,不经 profile 的 node_modules,remove 用不到它们身上(来源)。预期:dsh plugin --profile web list 输出里只剩内置包,--dump-config 里只剩官方那几层。想一次性清空并重置,可参考 一次性卸载所有 dsh plugin。
最后才轮到缓存与残留:只有在插件都卸干净、且确认不再需要旧版本之后,才去清 pnpm store 与 profile 里的残留配置。这一步做早了,回滚时得重新下载。清理范围见 卸载后残留怎么彻底清理。
每一步之后用什么命令确认 dsh plugin 顺序没错
验收口径只有一句话:包名必须同时从两份清单里消失。 每卸一步,按顺序查这四处:
- 查 pnpm 侧:
dsh plugin --profile web list | grep -n "<包名>"
无输出说明包已从依赖里移除;还有输出说明这一步没生效。
- 查加载层:
dsh --profile web --dump-config | grep -n "# =="
输出里不应再出现 # == <包名> 那一层。这一处才是「不再被加载」的判据,list 只说明包文件在不在。
- 查两份清单的原文:
cat ~/.dsh/profiles/web/package.json
包名应当既不在 dependencies,也不在 dsh.profile.bundles。
- 查你自己的覆盖层:profile 的
cordis.patch.yml里如果手写过引用该插件的行,要一并清掉——那层覆盖不受 pnpm 与组合包清单管理,remove不会替你动它。
四处都干净,再做下一步。批量卸载时每卸一个都过一遍,问题才能定位到具体那一步。
DSH plugin 卸载顺序的注意事项
记住主线:先停进程,先卸上层,后动底座,最后清缓存。
- 第一步永远是退出正在运行的实例:插件加载期间删文件最容易留下半截状态。
- 别用「先删目录、再补清单」的顺序:正确做法是让
dsh plugin remove一次清两样,手删目录只作为清残留的最后手段。 - 批量卸载先清上层:被依赖的底座包留到最后,否则会被 pnpm 拒绝移除,或留下悬空引用——这类情况见 卸载失败怎么办。
- 内置组合包不在清理范围内:它们随 dsh 本体分发,动不了也不该动。
- 卸载前先备份
package.json:出意外时照着它把清单恢复回去,比重新逐个安装快。 - 缓存与残留放到最后:插件还可能要重装时别急着
pnpm store prune。 - 只想临时关掉就别卸:用禁用代替卸载更快,区别见 关闭插件与卸载的区别。

常见问题
**卸载 dsh plugin 的顺序反了,最容易出现的后果是插件树加载失败。** 因为 profile 的两份清单是分开维护的:package.json 的 dependencies 记录包,dsh.profile.bundles 记录加载层。先把包删掉、清单原样保留,profile 就会照着清单去解析一个已经不存在的包,表现为启动时报 plugin tree failed to load。
**卸一批 dsh plugin 时,先卸「依赖别人的」上层插件,再卸「被别人依赖的」底座包。** 反过来先卸底座包,pnpm 可能因为还有别的插件依赖它而拒绝移除,或者留下一个引用了缺失包的插件;先清上层可以让依赖关系逐层解开,每一步都干净。
**卸载 dsh plugin 时,内置组合包不该动也动不了:@deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app、@deepseek-ai/dsh-headless 这类包始终从 dsh 安装本体解析,不经 profile 的 node_modules。** 想卸到只剩官方组合包,只要把 profile 里 out-of-tree 的插件卸干净即可,用 remove 去动内置组合包不会有结果。
**每卸一个 dsh plugin 后查两处:包名应当从 dependencies 与 dsh.profile.bundles 同时消失。** 具体做法是 dsh plugin --profile <名字> list 看包还在不在,再跑 dsh --profile <名字> --dump-config | grep "# ==" 看那一层还在不在;两处都没有了,这一步才算做对,可以继续下一步。
相关术语
- 移除顺序(removal order)
- 移除顺序指在 DSH 里卸载插件时的动作次序:先停掉正在加载插件的进程,再卸上层插件、后卸被依赖的底座包,最后才清 profile 目录与缓存。次序的意义在于始终让 profile 的两份清单指向真实存在的包。— https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md
- dsh.profile.bundles
- dsh.profile.bundles 是 profile 的 package.json 里 dsh.profile 清单中的有序组合包数组,决定启动该 profile 时按顺序叠加哪些插件层。它与 dependencies 必须同进同出,否则清单会指向一个已经不在的包。— https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md
- out-of-tree 插件
- out-of-tree 插件是装在 profile 的 node_modules 里、由 pnpm 管理的插件,与随 dsh 安装本体分发的内置组合包相对。只有 out-of-tree 插件能被 dsh plugin remove 卸掉,清理顺序讨论的也是它们。— https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md
- 依赖拒绝移除(blocked by dependency)
- 依赖拒绝移除是 pnpm 的保护行为:当某个包仍被项目内其他依赖引用时,直接移除它会丢掉被依赖方所需的解析路径,因此 pnpm 会拒绝或在移除后留下悬空引用。批量卸载时先清上层正是为了绕开这一点。— https://pnpm.io/cli/remove