
先说结论:在 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_define、cordis_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-greet、cordis.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。
常见坑

@deepseek-ai/dsh 不 pin 版本就装,装回来一个空壳;插件里的 @deepseek-ai/* 也必须和宿主 rc 一致,不然类型对不上。lib/,你改的是 src/,不 build 等于没改——而且它不报错,只是静默地用旧代码。dsh plugin add。 根是 pnpm workspace ,不是插件。要装也是按包路径:dsh plugin --profile web add github:kedoupi/dsh-plugins#path:plugins/greet。apply() 外面搞副作用。 Cordis 的插件卸载时要能清干净,副作用必须包在 ctx.effect() 里, disposer 要接住。下一步
插件能挂上只是开始。想加设置页,用 --kind mixed 生成模板,设置页按职责起名(比如「模型」),别拿包名当页名。想分发给别人,每个插件单独 pnpm publish 或 pack, Git 安装的写法是 github:你的仓库#path:plugins/<slug>。
写一个自己的 DSH 插件,比我想象的快很多。模板替你省掉了脚手架,剩下的就是写 apply()、 build 、 link 、重启。十分钟,够跑通第一个了。后面想深入,去翻 cordis-plugin-development 那个 skill 的说明,动态插件、主题、 Slot 全在里面。当然,那是另一个故事了。
夜雨聆风