Skip to content

CH 18 · 第一个插件 hello-plugin

全文字数2986 字预估耗时约 15 分钟前置CH 08(插件树)、CH 17(插件安装)难度可照做

本章目标

CH 17 你已经会装别人做好的插件了。这一章开始自己写——目标是最小的一个:写出一个 hello-plugin,让它真的被 dsh 加载起来。你不写工具、不碰界面,只验证一件事:你写的插件,能被 dsh 发现、加载、跑起来。这一步打通了,后面 CH 19 到 CH 23 那些花活(工具、钩子、UI、发布)都是在它基础上长出来的。

先记一个心态:插件不神秘。CH 08 说过"一切皆插件",那反过来——你想让 dsh 多一个能力,就是写一个导出 apply 函数的小模块。这就是插件的全部骨架。

动手:写一个 hello-plugin

在你想要的工作区里建目录(先 cd 到那个目录,再执行):

powershell
New-Item -ItemType Directory -Path "hello-plugin\src" -Force

然后新建 hello-plugin\src\hello-plugin.js,写:

js
export const name = 'hello-plugin'

export function apply(ctx) {
  console.log('[hello-plugin] plugin loaded!')
}

就这两行核心逻辑:插件被加载时打印一句 [hello-plugin] plugin loaded!。能打出来,就证明"你的代码被 dsh 跑起来了"——这是第一个里程碑。

这里我们用 JS 而不是官方示例的 TS:全局安装的 dsh 没有内置 tsx 运行时,直接加载 .ts 会报错;用 .js 免构建、零依赖,五分钟跑通。等我们写了更复杂的插件,再引入 TypeScript 和构建链(CH 20 会展开)。

把它加载进 dsh

光有文件 dsh 不知道要加载它。需要一个"覆盖层"告诉 dsh:额外加载这个插件。新建 hello-plugin\cordis.yml(下面 name 里的路径是示例,换成你自己的目录,注意点见后):

yaml
- insert:
    - id: hello
      name: 'file:///E:/software-workspace/DeepSeek%20harness%20demo/hello-plugin/src/hello-plugin.js'

三个注意点:

  • name 必须是 file:// 开头的完整 URL,不能写 E:\...E:/...。Windows 上 dsh 的模块加载器只认 file:///E:/... 这种形式,直接写盘符路径会报 Only URLs with a scheme in: file, data, and node are supported
  • 路径里有空格要编码成 %20。比如你的目录是 my work space,就要写成 my%20work%20space
  • 这是绝对路径。patch 文件只贡献配置,模块的解析根还是 profile 目录,所以本地插件必须写全路径。

先跑一次 headless 快速验证

不碰 Web UI,先用 headless 验证插件真的被加载:

powershell
cd 你的工作区目录
dsh --profile headless --patch "./hello-plugin/cordis.yml" "只回复:hi"

输出里你会看到这样两行:

text
[hello-plugin] plugin loaded!
hi

第一行是你的插件在启动时打印的,第二行是模型完成任务后的回复。看到 [hello-plugin] plugin loaded!,你的第一个插件就跑通了。

再加载进 Web UI

headless 能验证,但插件最终是要在 Web UI 里用的。先把正在跑的 dsh web 停掉(否则端口占用),再带 patch 启动:

powershell
dsh web --patch "./hello-plugin/cordis.yml"

打开 http://127.0.0.1:3080,启动 dsh 的那个终端里同样会打印 [hello-plugin] plugin loaded!。hello-plugin 暂时没有界面效果,它唯一的"产出"就是这句日志——但这证明它已经进入了 web 的插件树,和 CH 08 讲的那棵树的成员平起平坐。

看终端输出:第一行是 dsh 启动时打印的插件加载日志,下面两行是 Web UI 的就绪信息。

卸载时自动清理

ctx 注册的一切(事件监听、工具、定时器)在插件卸载时都会被框架自动清理,你不需要手动 removeListenerclearInterval。如果确实有需要手动释放的资源(比如一个网络连接),用 ctx.effect() 告诉框架怎么清理:

js
export function apply(ctx) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // 返回的函数会在插件卸载时执行
    return () => clearInterval(timer)
  })
}

effect 返回的清理函数,会在插件卸载那一刻被调用——这是 dsh 帮你管资源生命周期的标准姿势。

声明依赖:inject

如果插件要用别的能力(比如 toolsllm),要声明 inject,框架会保证依赖就绪后才加载你的插件:

js
export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx) {
  // 到这里 ctx.tools 一定可用
  ctx.tools.register(/* ... */)
}

inject 是 Cordis 里"服务依赖"的入口。现在先混个脸熟,CH 20 写工具插件时会正式用到。

插件的三种形态

apply 函数是最常见的形态,但插件支持三种写法(CH 19 会逐个细讲,这里先看一眼全貌):

形态长什么样什么时候用
函数export function apply(ctx) {}默认选择,本教程用的就是它
对象export default { name, apply(ctx) {} }想顺带带点静态元数据时
export default class extends Service {}给其他插件提供服务时(CH 19 展开)

现在记住一句就够:函数形态能解决 90% 的需求,服务形态留给"你的插件要被别的插件依赖"时再用

让 dsh 自己搭:一条提示词搞定

上面这些步骤你自己手动走了一遍,该学的都学到了。但 dsh 本身就是个 Agent——写插件这种活它也能干,而且是"它自己写、自己验证"。

直接在 Web UI 的输入框发这条提示词:

text
在我当前的工作区帮我写一个最小的 hello-plugin 插件,让 dsh 能加载它。你先去 dsh 官方仓库把插件开发文档读一遍,搞清楚插件应该怎么写、怎么被加载,然后按官方规范实现,并自己验证它确实被加载了,最后告诉我结果和文件放在哪。

你不需要告诉它任何技术细节——它自己会去读官方插件开发文档、自己决定怎么写、怎么加载、怎么验证。你只负责看它干活,然后打开文件检查它写的对不对。这正是"一切皆插件"的延伸:写插件的活,也能交给一个插件拼出来的 Agent 去干

我实际跑了一次,右上角看它自己列出的参考资料和产出的文件(package.jsonindex.jscordis.patch.yml)。它写完后,右侧文件面板直接就能看到工作区里新出现的 hello-plugin 目录和里面的文件——这正是 CH 17 装的侧边栏插件派上用场的地方,不用切到文件管理器就能核对它写了什么。

常见坑

问题怎么回事怎么处理
Only URLs with a scheme in: file...Windows 上路径写成了盘符形式name 改成 file:///E:/... 的完整 URL
报找不到文件 / 模块路径里有空格没编码空格写成 %20
patch 后重启 dsh 没反应?插件路径或 yml 拼写错了检查 idname 两处拼写,用 dsh --profile web --patch ./hello-plugin/cordis.yml --dump-config 看插件有没有进配置树
直接加载 .ts 报错?全局 dsh 没内置 tsx 运行时先用 .js 免构建跑通,需要 TS 时先构建成 .js 再加载
端口被占用起不来?之前的 dsh web 还在跑先停掉旧进程再启动

这一章你学到了什么

能自己完成下面几条,就算过关:

  • [ ] 能说出插件的最小形态:一个导出 apply(ctx) 函数的模块
  • [ ] 会建一个 hello-plugin,并在 cordis.yml 里用 file:// URL 声明它
  • [ ] 会用 dsh --profile headless --patch ... 快速验证插件被加载
  • [ ] 会带 --patch 启动 web,让插件进入 Web UI 的插件树
  • [ ] 知道 ctx.effect() 做资源清理、inject 声明服务依赖、插件有函数/对象/类三种形态

Open Source · MIT · Community Driven