写一个 DSH 插件(bundle 声明 + patch 层)
别名:插件骨架、写 DSH 插件、bundle 声明、dsh.bundle.patch
从零讲清一个 DSH 插件到底要声明什么:package.json 里的 dsh.bundle.patch、一份 YAML 补丁数组、宿主半端与可选的客户端半端,以及本地怎么挂上去验证——顺带说清「它为什么不是普通 npm 依赖」。
先弄清一件事:插件不是普通的依赖
装到 profile 里的包有两种命运。一种是普通库:装上了,被谁的代码 import 才起作用。另一种是层:它自己带来一段补丁,参与那棵插件树的装配。DSH 判断依据只有一句——这个包的 package.json 里有没有 dsh.bundle。
所以「写一个 DSH 插件」要交的作业不是一份功能代码,而是:一段声明 + 一份补丁 + 一个能被当成 cordis 插件挂起来的入口。@deepseek-ai/dsh-app-boot 的 loadProfile 对这段声明的处理很直接:解析不出 dsh.bundle.patch 就抛错(declares no dsh.bundle in its package.json),不会「当作没有补丁」放过去。
骨架:三样东西
my-plugin/
├── package.json # 声明自己是 bundle,并指出补丁文件
├── cordis.patch.yml # 那层补丁(顶层 YAML 数组)
└── lib/host.js # 宿主半端入口:一个 cordis 插件
package.json 的最小声明:
{
"name": "my-dsh-plugin",
"version": "0.1.0",
"type": "module",
"main": "lib/host.js",
"files": ["lib", "cordis.patch.yml"],
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" }
}
}
要点只有三条,但每一条都有人踩:
dsh.bundle.patch是「我是层」的唯一凭证。少了它,dsh plugin add会成功(包确实装上了),但只会给你一行告警:declares no dsh.bundle — installed as a plain dependency, not a profile layer。装上了不等于生效。- 补丁文件是顶层 YAML 数组,不是对象。写成一个带键的 mapping 会直接解析失败。
- 入口要能被当作 cordis 插件挂起来:要么导出
apply,要么是带name/inject/apply的形状。它跑在启动 DSH 的那个 Node 进程里,不是在浏览器里。
补丁里写什么:两种操作
补丁列表每项要么插行、要么改行,语义与层序的权威说明在 concept/1,这里给两种最常见的写法:
# 1) 插一行:把宿主插件挂进树
- insert:
- id: my-plugin-host # 这行自己的 id,别的补丁靠它定位
name: my-dsh-plugin # 可解析的包名
inject: [webServer] # 我依赖哪些服务(服务名是 DSH 的,写错就起不来)
# 2) 改一行:按 id 改已有行的顶层键
- id: some-existing-row
disabled: true # 摘掉一行
两个必须记住的边界:
config是整段替换,不是深合并。只想改一个键,也要把想保留的键一起写全;否则新版给这一行补的键会被你这份 config 抹掉。这是 tutorial/1 里「插件在、功能没了」那一类失效的直接成因。- 命不中的补丁只告警、不报错。这对启动稳定性是好事,对你调试是坏事——写完一定要看启动输出,别只看退出码。
真实样本:一个包两半怎么声明
plugin/2 的 package.json 是最省事的形态:宿主侧靠 cordis.patch.yml,客户端侧靠 dsh.client,两段都在同一个包里。
"exports": {
".": { "default": "./lib/host.js" },
"./client": "./lib/client.js",
"./cordis.patch.yml": "./cordis.patch.yml"
},
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-runtime", "…"] }
}
它的 cordis.patch.yml 短到只有一行 insert,宿主插件按 id dspack-host 挂上去,inject 写 [connection, webServer, skills]。
需要两半还是只要宿主半端,取决于你有没有界面:纯后端能力(工具、采集、索引)一个宿主半端就够;要往侧边栏、设置页、输入框附近加东西,才需要客户端半端——它的声明字段、./client 产物、window.__DSH_BOOT__ 入口图的完整规则见 concept/4,这里不重复。
dsh-packforge-app 的 monorepo 是另一种组织方式:把插件放在 packages/plugin,与打包引擎(core)、命令行(cli)、Electron 界面(gui)并列,带 dsh 声明的仍然只有插件那一个包。「哪个包才是插件」与「我在哪个目录开发」是两件事,别把 workspace 根当成插件根。
本地怎么挂上去验证
不需要先发布。profile 的插件管理本质上是「在该 profile 目录里跑 pnpm」,所以本地路径可以直接装:
dsh plugin --profile tui add /abs/path/to/my-plugin # 绝对路径最稳
dsh --profile tui --dump-config # 看组合后的树:你的行进来了吗
dsh --profile tui --dump-default-config # 只看 bundle 层,不含用户层
dsh --profile tui # 真正启动
三点提示:
- 用绝对路径。
dsh plugin会把./../plugin这类相对路径按你当前所在目录重写;写成绝对路径就不用记这条规则。 config想改就写进 profile 的cordis.patch.yml(或临时用--patch叠一个文件),不要去改插件目录里那份——插件那份是随包分发的,你改了下次更新就没了。- GitHub 坐标来源会在安装时跑构建脚本:pnpm 默认拦下
prepare,第一次装会失败并打印一个键名,把它加进该 profile 的pnpm-workspace.yaml的allowBuilds再装一次。卸载、落点与这条授权链路见 tutorial/3。
常见坑清单
| 现象 | 大概率原因 |
|---|---|
| 安装成功,启动后毫无变化 | 少了 dsh.bundle.patch,被当成普通依赖 |
启动时抛 declares no dsh.bundle | 被列进了 dsh.profile.bundles,但包本身没有声明 |
| 补丁文件解析失败 | 顶层写成了对象;它必须是数组 |
| 启动报错说找不到某个服务 | inject 里的服务名不是 DSH 实际提供的(大小写与命名都要对) |
| 配置项莫名回默认 | 补丁里的 config 整段替换,把新版的键抹掉了 |
| 插件两半只活一半 | 宿主那一行没挂上,客户端入口图里自然也没有它 |
| 界面上的东西没出现 | 声明写了但 exports["./client"] 指不到真实产物文件 |
插件装好之后每次启动到底走了哪几步、为什么会有该 profile 专属的 node_modules,见 tutorial/3。
未核实 / 未声明
- 「插件的入口可以是
apply之外什么形状」:本轮只核实了dsh.bundle.patch的解析与补丁语义,没有逐条核实 cordis 插件入口的全部合法形态;上面那条按最小可用写法给。 dsh.client的字段全集:以 concept/4 的核对为准(platform必需,inject/immediately可选);本教程没有独立复核。- 发布与收录的完整流程:不在本篇范围内,本站另有整合包发布向的内容;本篇到「本地挂上去能跑」为止。
相关教程
谁引用了这一条
信息表
- appliesTo
- dsh 0.1.0-rc.6(实测,本机 @deepseek-ai/dsh 与 @deepseek-ai/dsh-app-boot 的实际实现);插件声明格式由 @deepseek-ai/dsh-app-boot 的 loadProfile 决定,更高版本未核实
- category
- ecosystem.publish
- difficulty
- intermediate
- origin
- original
- plugins
- [object Object]、[object Object]、[object Object]
- prereq
- concept/1、concept/4
- related
- tutorial/1、tutorial/3
- updatedAt
- 2026-10-02
标签:插件、开发、bundle、patch 层