dsh 插件开发学习

02-最小插件的结构与配置

读完这篇你能:写出一个最小插件,装进 test profile,并在启动时看到它打印 hello world。

前置:01-profile、bundle 与插件的关系。约 15 分钟。

示例代码:dsh-hello-world

DSH 版本:0.2.0-rc.2

目标

写一个最小插件,让它在 DSH 的 test profile 启动时打印一句 hello world。

DSH 设计

01-profile、bundle 与插件的关系已经讲过配置体系的三层概念:插件是功能本身,组合包(bundle)是它的分发格式,profile 是决定哪些分发进入本次启动的方案。落到「写一个插件」这件事上,只需要再记住一条:一个插件进入 DSH,需要的全部东西是代码文件加一份挂载补丁。

这条要求来自启动过程本身。DSH 启动时从空清单开始,把各层补丁逐层叠上去,叠完的那张清单就是插件树。插件想进树,唯一要做的就是让某一层补丁里出现指向自己代码的那一行。插件包的 package.json 用 dsh.bundle 字段声明自己的补丁在哪个文件;把这个字段指对了,剩下的就是写代码本身。

补丁按什么顺序叠、每一层各管什么,见03-用 patch 覆盖 DSH 的默认配置;「行」怎么叠成「树」,见由行组成的树。

目录结构

dsh-hello-world/                         ← 插件包(bundle)
├── dsh-hello-world.ts                   ← 插件本体:name + apply
├── package.json                        ← 身份 + dsh.bundle 声明
└── patch.yaml                          ← 挂载指令

最小插件

// dsh-hello-world.ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'dsh-hello-world'

export function apply(ctx: Context) {
    console.log('hello world')
}
  • name:插件的唯一 id
  • apply:挂载时执行什么

仅此而已。这就是一个合法插件。

但是要区分一下注册和挂载两个动作,注册是我们将 bundle 添加到profile 中。挂载是运行时概念,dsh 启动时将某个功能挂载到 dsh 的运行时上下文中。

bundle 声明

光有代码不够,DSH 不知道”这段代码该怎么进插件树”。所以一个 bundle 需要两个文件:

package.json

{
  "name": "dsh-hello-world",
  "version": "0.1.0",
  "type": "module",
  "dependencies": {
    "@deepseek-ai/cordis": "^4.0.1"
  },
  "dsh": {
    "bundle": {
      "patch": "./patch.yaml"
    }
  }
}

注意最后那个 dsh.bundle.patch——这是 DSH 专属字段,npm/pnpm 完全不认识它。DSH 靠它找到补丁文件。

patch.yaml

- insert:
    - id: dsh-hello-world
      name: '/绝对/路径/dsh-hello-world.ts'

意思是:往插件树里插一行——id 叫 dsh-hello-world,代码在哪个文件。

patch.yaml 这个名字是我们自己起的。内核不认文件名,只认 package.json 里 dsh.bundle.patch 指到哪个文件——上面写 ./patch.yaml,所以读这个;改成 ./挂载.yaml 也照样跑,官方模板里同一个文件叫 cordis.patch.yml。

名字里有 cordis.patch.yml 时别搞混:它只有在 profile 目录(~/.dsh/profiles/<名字>/)和 ~/.dsh/ 下才是内核写死的约定(用户覆盖层,自动读取);插件包里的那份和本文的 patch.yaml 是同一个角色,只是被 dsh.bundle.patch 声明进树。

dsh.bundle.patch 的值可以是一个文件,也可以是一个有序数组:同一个包想按不同界面/场景挂不同的行,就备多份补丁文件,让命令行或环境决定读哪一份。

mermaid 图表

小坑:name 写相对路径时要以 ./ 开头。以 ./、../ 开头的相对路径会按 patch.yaml 自己所在的目录锚成 file:// URL;裸写 dsh-hello-world.ts 会被当成包名去解析,找不到文件。

安装到 profile

cd 你的插件目录
dsh plugin --profile test add link:.

这条命令做了三件事:

  1. 把 link:. 翻译成插件目录的绝对路径
  2. 在 profile 目录里执行 pnpm add(是的,它只是 pnpm 的”转手人”)
  3. 检查装上的依赖里哪些声明了 dsh.bundle,把它们的包名追加进 profile 的 dsh.profile.bundles

注意第 2 步的 cwd 是 profile 目录,不是你的插件目录。所以它只改 profile 的配置清单,永远不碰插件自己的 package.json——插件代码要 import 的包,得你在插件目录里自己加。

验证

dsh --profile test --dump-config

这条命令会把组合好的插件树打印出来。看到这一节就成功了:

# == dsh-hello-world
- id: dsh-hello-world
  name: file:///绝对路径/dsh-hello-world.ts

然后启动 DSH,日志里出现 hello world。