第一个插件
一句话版:一个插件 = 一个导出
name/apply(ctx, config)的模块 + package.json 的dsh清单字段 + 挂进 profile:从 hello world 到能用,十分钟。
这是 插件解剖 的动手篇。实操闭环对应官方教程 docs/user/develop/basic(本课)、tool.zh.md、config.zh.md、publish.zh.md。读完你会写、会挂、会验证一个插件。
零、前置
- Node:
^22.19.0 || >=24.0.0(官方根package.json#engines,审计基线 rc.7 @ 99f6f02) - 源码检出:scratch 挂载流需要完成官方 README 的 run-from-source(clone +
pnpm install+pnpm run build),随后在检出根目录用pnpm dsh …;只装 CLI 走npx @deepseek-ai/dsh web(见 快速上手) - 包管理器:pnpm(推荐)
一、插件的形状
DSH 插件本质是 cordis 插件:一个导出 name 和 apply(ctx, config) 的模块(或一个继承 Service 的 class),外加 package.json 里 dsh 清单字段告诉 loader"怎么挂载、依赖什么"。
my-plugin/
├── package.json # 清单(含 dsh 声明)
├── src/index.ts # 插件本体
└── (可选) cordis.patch.yml # bundle 型才需要:要插入哪些行
二、最小插件(监听一个事件)
import { Context } from '@deepseek-ai/cordis'
export const name = 'hello-dsh'
export function apply(ctx: Context) {
ctx.on('session/created', () => {
console.log('[hello-dsh] 新会话!')
})
}
name是插件在组合里的唯一 id(也用于 patch 定位)apply(ctx, config)是入口;注册的监听器 / 工具随调用 fiber 卸载自动清理
三、官方 scratch 挂载流(--patch 覆盖层)
官方"第一个插件"教程的主路径:临时目录写插件,用 --patch 覆盖层插进 Web 组合,不需要打包:
# 在 Harness 仓库根目录
mkdir -p scratch-plugin/src
写 scratch-plugin/src/my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// 依赖服务就绪后 apply 才执行
console.log('[hello-plugin] plugin loaded!')
}
在仓库根目录跑 pwd,把打印出的绝对路径填进 scratch-plugin/cordis.yml:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
启动并验证:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# 打开 http://127.0.0.1:3080,终端出现 [hello-plugin] plugin loaded!
两条约束(官方原文):
- 插件路径必须是绝对路径
- patch 文件只贡献配置,不会改变 loader 解析模块路径所用的 profile 目录
这就是 bundle 型插件 patch 层的同款 YAML 语法(
- insert:+id/name),差别只是name填本地源文件路径而非包名。见 插件解剖。
四、更完整的例子(有 config + 工具)
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greeter'
export const inject = ['tools'] // 声明要用 tools
export function apply(ctx: Context, config: { greeting: string }) {
ctx.tools.register(defineTool({
name: 'say_hi',
description: '按插件配置的问候语打招呼',
parameters: { to: { type: 'string' } },
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: String(v) }] },
async execute({ to }) {
return `${config.greeting}, ${to}!` // 与 output.schema:{type:'string'} 一致,返回字符串
},
}))
}
挂上后,模型就多了一个 say_hi 工具,返回值带你的问候语。真正的 Config 校验与默认值走 Schemastery schema(见 配置与发布),这里内联类型只够演示。
五、自动清理与 ctx.effect
通过 ctx 注册的任何东西——事件监听、工具、定时器——在插件卸载时都会自动清理,不需要手动 removeListener / clearInterval。需要手动释放的资源(网络连接、文件句柄)用 ctx.effect() 交清理函数:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// 返回的清理函数在插件卸载时运行
return () => clearInterval(timer)
})
}
六、三种形态
函数形式对大多数情况足够;类形式用于向其他插件提供服务(见 插件解剖 与 写一个服务)。对象形式介于两者之间:
import type { Context } from '@deepseek-ai/cordis'
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
类形式要点:constructor 里 super(ctx, 'myService'),且初始化必须是同步的。
七、清单声明(package.json)
{
"name": "@dsh-external/my-plugin",
"main": "lib/index.js",
"exports": { ".": "./lib/index.js" },
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
dsh.bundle.patch= 声明这是 bundle 型插件 + 它自带的 patch 文件(单数字符串路径,不是数组)- 其它形态(
dsh.client、dsh.profile.bundles)见 插件解剖
八、挂载方式(三选一)
# 1. bundle 型:git 源一行
dsh plugin --profile web add "github:dsh-external/my-plugin#main"
# 2. 本地目录开发
dsh plugin --profile web add link:/path/to/my-plugin
# 3. patch 层手动挂载(cordis.patch.yml)——同第三节 scratch 流的语法
- insert:
- id: hello-dsh
name: '@dsh-external/my-plugin'
九、profile 目录与闭包解析
dsh plugin --profile <name> ... 本质是在 profile 目录转发 pnpm:
- bundle 名双锚点解析(先安装目录、后 profile)
- 启动维护
$DSH_HOME/profiles/node_modules扁平闭包 fallback,让 out-of-tree 插件解析到同一个 cordis 实例 dsh.profile.bundles里列出的 bundle 会在 reconcile 时自动激活
这也是为什么"裸名依赖会解析失败":框架闭包只含
@deepseek-ai/*,详见 插件解剖。
十、开发循环
# 构建 TS(tsdown/tsc)
pnpm build
# 验证组合里能看到插件
dsh web --dump-config | grep hello-dsh
# 重启生效(bundle 型需重启;--patch 覆盖层改动也需重启)
dsh web
热更新边界(源码核对):每次启动由 watchUserPatches 热应用的只有 profile 自身的 cordis.patch.yml——编辑它,框架卸载旧插件实例、按新配置加载(读/解析失败则保留上一个好树并广播 hmr/config-update-failed)。--patch 覆盖层与 bundle 的 patch 文件改动需重启生效。
十一、常见坑
| 坑 | 解法 |
|---|---|
裸名 cordis/schemastery 解析失败 | 用 @deepseek-ai/cordis;必要时把裸名装进插件自身 node_modules |
| 未声明 inject 就访问服务 | Proxy 拒绝:先 inject: ['tools'] 再 ctx.tools |
patch 层 name 不符 | 静默跳过,条不生效:检查 id/name 是否与目标行一致 |
| config 是整行替换 | patch 没给全的字段走 schema 默认;别期望深合并 |
--patch 路径写相对路径 | 官方要求绝对路径:用 pwd 的结果拼 |
只导 Config interface 不导同名 schema | 无校验无默认值;官方要求导出 Schemastery schema(见 配置与发布) |
下一步
参考
- 官方教程:docs/user/develop/basic(本课内容已按 rc.7 源码逐点核对)
- 社区整理:How-DSH-Plugin-Made(本章的 scratch 挂载流与结构参考该文档)