DeepSeek Harness在自己官网上的口号是:一切皆插件
在默认配置下启动这个系统,你能看到它加载了159个插件。

如果不加载这159个插件,当前应用会剩下什么呢?
根据代码分析,当你看到dsh界面时,并不是一个程序+159个附件,而是这个程序本身由159个插件组成。
所以,如果不加载这159个插件。dsh只剩下一个空架子,这里“空”架子的“空”是字面意义上的空,不是说已经搭好架子,只要有插件放入就能实现功能,而是架子本身也是插件。没有这159个插件,只会在电脑里留下一下空转的进程,这些进程只是插件的加载代码。包括:
- ◆Node 进程 + apps/cli 的 bin 脚本
- ◆dsh-app-boot 启动胶水(建根 Context、装 Loader、挂 Include 树、失败时报错退出)
- ◆vendor 目录里的 Cordis 框架库(Context/Fiber/事件系统/Loader/Include)
也就是说,唯一不是插件的东西就是框架库本身,而它只提供“装插件”这件事。
既然这样,我们完全可以在不修改任何代码的情况下,只修改配置文件,就能定制一套不同的系统。
比如,当前是默认配置,我新建一个配置文件,加载不同的插件,另外命名,然后我在启动时,用参数指定新建的配置文件名,就能新建另一个dsh实例。
通过配置文件开一个不同的dsh
profile 机制的设计用途——全程零代码改动,只有 JSON/YAML 配置。
- ◆一个
profile = $DSH_HOME/profiles/<name>/目录里的一个 package.json(声明 dsh.profile.bundles 层列表)+ 一个 cordis.patch.yml(你自己的覆盖层)。 - ◆启动用
dsh --profile <name>;--port 3090 之类的是 web app 自己的参数,跟在 profile 之后:dsh --profile <name> --port 3090(CLI 文档原例:dsh --profile web --port 8080)。 - ◆非 web/headless 的新名字必须先创建:
dsh plugin --profile <name> <pnpm 参数>会初始化目录,之后你手动编辑 manifest 和 patch 即可。
# 1. 独立 home$env:DSH_HOME = "$HOME\.dsh-two"# 2. 初始化新 profile(先以 base 为模板)dsh plugin --profile minimal-web install# 3. 编辑 $HOME\.dsh-two\profiles\minimal-web\package.json:# 把 dsh.profile.bundles 改为 ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]# 4. 新建 $HOME\.dsh-two\profiles\minimal-web\cordis.patch.yml:# - id: agent-presets# config:# default: minimal# 5. 预览合成树(可选)dsh --profile minimal-web --dump-config# 6. 启动dsh --profile minimal-web --port 3090启动后,我们能看到在3090和3080两个端口各开了一个dsh,新开的dsh中插件数量与原来的不同了。这次只有133个

理解插件的加载过程
我们现在知道了,整个dsh是一步步加载插件后成型的。那么它到底是怎么加载的呢?我这里就以默认的159个插件为例说明其加载过程。
第一步,纯数据合成(不执行任何插件代码)
- ◆从空条目列表开始,按固定层序应用 patch:dsh-base → dsh-web-app → profile 的 cordis.patch.yml → home 级 → --patch overlay。
- ◆后面的层按 id 找到前面的行,整行替换其 config,新覆盖旧。这一步结束后得到最终 129 行的清单。
- ◆关键是这一步只有 YAML 数据操作,没有任何模块被 import、没有 apply() 被调用。
--dump-config就是把这个纯函数离线重放一遍(boot 与 dump 共用同一个 applyEntryPatches,代码注释明确说“composition、flag derivation 与 config dump 不能与 boot 漂移”)。
第二步,挂载(不再重新合成)
- ◆Loader 拿第一步得到的最终清单一次性挂载;挂载过程中不会再回头做“重新覆盖”——所有覆盖都在阶段一完成。
第一步时,是有顺序的,因为涉及覆盖。第二步,其实是并发的,但是如果插件声明了依赖,就pending,等服务就绪后才apply().
唯一的例外:运行中的热重载
启动之后,profile 的 cordis.patch.yml 被 watcher 监视。你运行时改这个文件,才会发生“加载后再覆盖”:Loader 做事务性协调——新配置先挂载候选,成功后才卸载旧条目,失败则回滚保留旧树,而且只动受影响的条目,不整树重载。这是显式的 live-reload 路径,不属于普通启动流程。
一句话总结:合成(第一步)是顺序的、纯数据的、新的覆盖老的;挂载(第二步)是并发的、依赖门控的、不再覆盖。
插件可以动态启停吗?会有冲突吗?
我们在看到默认的插件列表时,会发现有一些插件是禁用的。但是并没有提供一个启用开关。
首先,我们要确定一点,插件是会引发冲突的。前面我们说过,插件的加载是并发的,但实际加载过程中还是有顺序的,因为一个插件可能会有依赖项,在依赖的服务没有加载之前,插件只能处于pending状态等待前驱的服务就绪。
所以如果我们禁用的插件有依赖它的插件,会让这些插件也被停用。而这些插件的停用对你而言,是非预期的。
当前界面并没有给禁用开关,但是根据代码中Cordis调用eb服务的机制,服务是可以在运行时被关闭的。修改cordis.patch.yml文件就可以。
根据它的删除机制(vendor/cordis/src/reflect.ts),Cordis从yml文件中得到关闭通知时,会沿着生成树,向枝端传播,先删除最枝头的服务,后通知,再删除下一层,直到完成所有工作。
就以当前默认配置的插件列表来看,我们看到的第一个被停用的插件是hmr。
HMR(Hot Module Replacement,模块热替换)可以保证模块动态加载,在运行时热替换。为什么被停用呢?是不是说dsh没有热重载功能?
当然不是,系统只用另一个服务代替它了而已。停用了base里的hmr,但由一个运行时补挂的hmr实例接管了。你搜索一下,就能看到,后面有另一个hmr插件是已启用状态。后者是启动时补挂的 watch-only hmr 实例。这就解释了,为什么改插件代码不会热重载,而修改cordis.patch.yml就可以即时生效。
插件列表
这时,我们可以理解这里看到插件列表和我们在其它Agent中看到的插件列表的区别了。
其它的Agnet里,插件列表列出的是附加的功能。我们能从中看到这个Agent里加挂了哪些功能,比如搜索网站、读写Github等。而这里的插件列表中当前也可以包括这些内容,但它把这个系统所有的组成部分都列出来了。更像是windows任务管理器中的进程管理。
夜雨聆风