乐于分享
好东西不私藏

十分钟上手DSH插件

十分钟上手DSH插件

先说结论:在 DeepSeek Harness (下面叫 DSH )里写插件,门槛比你想的低。不碰 C++,不碰 Rust ,你甚至可以不碰前端。前提是你得先搞清楚它插件系统的基本盘: profile 和 bundle 。老实讲,我第一次接触这套东西是懵的。文档分散在好几个仓库里, profile 、 bundle 、 patch 三个词叠在一起,谁也说不清谁是谁。

前置准备

DSH 的命令行是 dsh,装它一条命令的事:

pnpm add -g @deepseek-ai/dsh@0.1.0-rc.7 

注意版本。很多 @deepseek-ai/dsh-* 的 latest 还停在空壳 0.0.1-rc.1,安装必须写死版本,不然你会拿到一个啥都没有的包。这个坑我踩过,装完 dsh web 直接报模块缺失。

装完确认一下:dsh --help 能出东西就算成了。

先搞懂两个概念

DSH 的插件有两条开发路线,别混:

路线 A :会话内动态插件。 你在对话里让 AI 直接写一个 Cordis 插件,用 cordis_definecordis_run 这套工具现场定义、现场跑。适合快速验证想法,生命周期跟着会话走。

路线 B : npm 插件包。 把插件做成一个可独立安装的包,装进 profile 。这才是"开发插件"的完整姿势,本文走这条路。

什么是 profile ?就是 DSH 的一套启动配置,里面声明了一串有序的 bundle (补丁层)。dsh web 用的就是 web profile 。插件本质上就是往这个补丁栈里插一层。

用模板起一个新插件

官方推荐的做法是 monorepo + 模板。我的工作区长这样:

dsh-plugins/ ├── plugins/          # 可发布的插件,包名 dsh-<slug> ├── packages/         # 内部库(没有 dsh.bundle) ├── templates/        # pnpm new 的骨架 └── .dsh-home/        # 沙箱 Harness 家目录(gitignore) 

创建插件不要手写目录,跑模板:

pnpm new greet                 # 默认 host:工具/服务,无 UI pnpm new sidebar --kind mixed  # 有设置页/Slot/主题才用 mixed 

生成出来的骨架里有个 greet 样例,直接删掉换成你的逻辑。但几个名字必须保持一致,少一个后面就挂不上——目录叫 plugins/greet/package.json 的 name 是 dsh-greetcordis.patch.yml 里的 name 也是 dsh-greet、 patch 的 id 是 greet。这事我第一次就漏了 patch 里的 id ,装上去毫无反应, dump-config 里怎么都找不到这一层,排查了半天。

写第一个工具

Host 插件的核心就一个 apply(ctx),往里注册东西。模板里的样例长这样:

export const name = "greet"; export const inject = ["tools"];  export function apply(ctx: ToolHost, config: Config) {   ctx.tools.register({     name: "greet",     description: "Greet someone by name.",     parameters: {       type: "object",       properties: { who: { type: "string", description: "Name to greet" } },       required: ["who"],     },     async execute({ who }) {       return greet(who, config.greeting);     },   }); } 

注册到 ctx.tools 上的工具,下一轮模型就能直接调。纯逻辑部分(比如 greet() 那个函数)单独放一个文件,不要依赖 Cordis ,这样测试只用测纯函数,不用 mock 整个 harness 。说实话,这套约束我一开始觉得是小题大做,后来改了一次公共依赖,测试跟着炸了一片,才明白设计者的用意。

构建 + 挂进 profile

Profile 加载的是 lib/,不是 src/。所以改完源码必须重新构建,这是新手最容易忽略的一步。我见过有人改完代码重启三次还看到旧行为,最后发现是没 build 。就这么简单。但就是这么简单的一件事,能卡住你一整天。因为它报错报得一点都不明显,进程照常起,插件就是旧的。

pnpm --filter dsh-greet build node scripts/link-plugin.mjs --profile dsh-dev greet   # 先验证能不能挂上 

dsh-dev 只验证挂载和进程能不能起来。要在 Web UI 里点、要让模型真调工具,用 --profile web 然后 pnpm dev(端口 3081 )。

这里有个关键设计:开发沙箱和日常环境必须分开。日常的 dsh web 跑在 ~/.dsh(端口 3080 ),你开发用的 .dsh-home 是 gitignore 掉的沙箱,link-plugin 和 pnpm dev 会自动把 DSH_HOME 指过去。千万不要把开发插件挂进 ~/.dsh/profiles/web,不然日常环境会给你搞乱。别问我怎么知道的。

怎么确认装上了

跑 pnpm dev 之后,验证插件真的被加载:

pnpm dsh --profile web --dump-config | grep 'dsh-greet' 

输出里出现 # == dsh-greet 这一层,就说明补丁栈里已经有它了。改完代码 rebuild 之后,重启的是 pnpm dev,不是日常的 dsh web

常见坑

1.版本没写死。 @deepseek-ai/dsh 不 pin 版本就装,装回来一个空壳;插件里的 @deepseek-ai/* 也必须和宿主 rc 一致,不然类型对不上。
2.忘了 build 。 这条几乎人人中招: profile 加载的是 lib/,你改的是 src/,不 build 等于没改——而且它不报错,只是静默地用旧代码。
3.对仓库根执行 dsh plugin add。 根是 pnpm workspace ,不是插件。要装也是按包路径:dsh plugin --profile web add github:kedoupi/dsh-plugins#path:plugins/greet
4.在 apply() 外面搞副作用。 Cordis 的插件卸载时要能清干净,副作用必须包在 ctx.effect() 里, disposer 要接住。

下一步

插件能挂上只是开始。想加设置页,用 --kind mixed 生成模板,设置页按职责起名(比如「模型」),别拿包名当页名。想分发给别人,每个插件单独 pnpm publish 或 pack, Git 安装的写法是 github:你的仓库#path:plugins/<slug>

写一个自己的 DSH 插件,比我想象的快很多。模板替你省掉了脚手架,剩下的就是写 apply()、 build 、 link 、重启。十分钟,够跑通第一个了。后面想深入,去翻 cordis-plugin-development 那个 skill 的说明,动态插件、主题、 Slot 全在里面。当然,那是另一个故事了。