ARTICLE · 1041035
从零写一个 DSH 插件
一、DSH 是什么
DSH(DeepSeek Harness)是 DeepSeek 的 agent harness——它不是模型,而是驱动模型的那层运行时:agent loop 的编排、会话的持久化与回放、LLM 调用的抽象、工具的调度,都发生在这一层。
它有两个设计值得先记住。
服务化。运行时能力被拆成一组服务挂在容器上,插件按需声明后使用。比如 llm(模型调用与提供方注册)、sessions(会话存储)、sessionProjections(会话的只读派生视图)。@deepseek-ai/dsh-llm 这个包做的正是"提供方无关的 LLM 词汇与抽象服务"——上层是同一套调用接口,底下接哪个模型由适配器决定。
分 profile。DSH 按 profile 组织运行环境,例如 web UI 跑在 web profile 上。每个 profile 有自己的配置树和自己的 node_modules,互不干扰。下文所有 --profile web 就是这个意思。
二、DSH 插件是什么
一个 dsh 插件 =一个普通的 npm 包,包里装的是一段Cordis插件。Cordis 负责依赖注入和生命周期:ctx 随 fiber 销毁自动回收,插件不必自己写清理逻辑。
插件能做的事大致三类:
ctx.on(...) | ||
ctx.llm.registerAdapter(...) |
安装用 CLI(它是 pnpm 的薄转发器):
dsh plugin --profile web add <包名>三、DIY插件:dsh-welcome
例子选dsh-welcome:102 行、单文件,作用只有一个——每个新建的空会话,自动发一条欢迎消息「Hello,欢迎来到DSH」。选它是因为它小,却把上面说的机制全走了一遍。
未全局安装 dsh 时,用 npx 形式,将dsh 替换成npx @deepseek-ai/dsh
先跑通,后面的机制才有落点:
dsh plugin --profile web add dsh-welcomedsh web# web profile 禁用了 HMR,必须重启


新建一个会话,顶部出现一条正常的助手气泡就算通了。它不是带 plugin 标签的上下文注入,而是由助手"直接说出"的——做法是插件往会话日志里写一个完整、已闭合的助手轮次。

四、动手:从零搭出这个插件
4.1 先写骨架
一个 dsh 插件就是一个 ESM 模块,导出四样东西:
export const name = ”dsh-welcome”; // 插件名,也是配置行的默认 idexport const inject = [”sessions”, ”sessionProjections”]; // 声明要用到的服务export const Config = z.object({ // 可配置项(schemastery 声明)greeting: z.string().default(”Hello,欢迎来到DSH”)});export function apply(ctx, config) { // 插件主体ctx.on(”session/created”, handler, { global: true });}
四个导出各有分工:inject 声明服务依赖,没声明的服务在 ctx 上拿不到;Config 用 schemastery 描述,默认值即文档,宿主校验通过后才传进 apply;apply 里订阅事件、注册能力。
4.2 让它被加载
把 index.js 装进 profile、重启——大概率什么都不会发生。因为 dsh不扫目录,插件是否被启动完全由配置树决定:树里有一行 Loader,插件才会被加载。
插件通过 bundle patch 把自己这一行插进去。先在 package.json 声明:
{ ”dsh”: { ”bundle”: { ”patch”: ”./cordis.patch.yml” } } }再在 cordis.patch.yml 里插入 Loader 行:
- insert:- id: welcomename: 'dsh-welcome'
配置树是分层的,patch 应用顺序决定了谁覆盖谁:

dsh plugin add 之所以能"装完即用",是因为它除了跑 pnpm,还会自动把插件追加进 dsh.profile.bundles 列表——省了自己改配置。
4.3 往会话日志里写事件
插件不直接操作 UI,而是往会话日志里 append 事件。日志用 turn / step 两级坐标定位,一个最简的完整轮次长这样:

UI 和模型历史都只读这份日志,所以只要事件序列合法,界面就会照常渲染,插件无需碰任何 UI 代码——这也是"欢迎语能变成一条正常助手气泡"的原因。
不过"序列合法"这四个字是有硬性门槛的:turn 的取值、stream 字段、以及 session/created 的触发时机都有约束。
五、本地开发与调试
从本地源码安装:
dsh plugin --profile web add ”file:D:\path\to\my-plugin”
安装后必需重启 dsh web
改了源码后必须remove再add,update 是空操作。原因是 profile 用 nodeLinker: hoisted,node_modules/<包> 是安装时复制出来的真实目录(不是软链);而 pnpm 对 file: 目录依赖在 lockfile 里只记 version: file:<路径>,不含版本号,所以重跑 add/install(甚至 --force)都是 Already up to date。
dsh plugin --profile web remove my-plugindsh plugin --profile web add ”file:D:\path\to\my-plugin”
最后记得重启 dsh web——web profile 的 HMR 被禁用,不重启不生效。
六、测试
新建会话

示例插件的完整源码在github.com/axingde/dsh-welcome