跳到主要内容
路径文档

第一个插件

一句话版:一个插件 = 一个导出 name/apply(ctx, config) 的模块 + package.json 的 dsh 清单字段 + 挂进 profile:从 hello world 到能用,十分钟。

这是 插件解剖 的动手篇。实操闭环对应官方教程 docs/user/develop/basic(本课)、tool.zh.mdconfig.zh.mdpublish.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 插件:一个导出 nameapply(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) {
// ...
},
}

类形式要点:constructorsuper(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.clientdsh.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(见 配置与发布

下一步

参考