赞助商LobeHubLobeHub了解更多
ddshfind
GitHub

第 1 课:第一个插件:Hello, DSH!

一句话版:在 DSH 里给智能体加一个「打招呼」插件,不用 fork 源码——写一个导出 nameapply(ctx) 的 TypeScript 模块,在 cordis.yml 里登记它,启动 dsh 后它立刻生效;把它移出装配再启动,它注册的一切也随之还原。这就是你亲手写下的第一个插件。


1. 用户故事:让智能体学会打招呼

上一课(第三章 · 第 11 课)我们把一个 DSH 包从外到内解剖了一遍:目录、文件、组件定义、fiber。这一课开始动手——你坐在一台装好 DSH 的电脑前,Web UI 跑在 http://127.0.0.1:3080 上。你突然有个小愿望:

每次智能体启动时,让它先打一声招呼,说一句「Hello, DSH!」。

在传统框架里,这可能意味着 fork 源码、改主循环、重新编译。在 DSH 里,你只需要做三件事:

  1. 写代码:新建一个 TypeScript 文件,里面放一个插件;
  2. 注册:在一份叫 cordis.yml 的装配文件里登记它;
  3. 启动:框架加载插件,能力立刻生效。

这就是整个第四章的主线。把这四步画出来,就是你接下来反复走的插件开发循环

① 写代码inject / apply / ctx.use② 注册cordis.yml / profile③ 加载运行dsh 启动即生效④ 调试日志 / 事件改代码 → 热模块替换,无需重启

写插件 = 声明「需要什么 / 贡献什么」,注册即加载,加载即生效

注意图的最后一步:改代码 → 热模块替换,无需重启。你改完插件,系统会热更新,不用一遍遍重启——那是第四章后面「热替换」那一课的主角,这一课先把前三步走通。


2. 环境准备:克隆仓库、构建、拿到 dsh 命令

要写插件,先得有 DSH 本体。官方快速开始给的清单是(来源:docs/user/guide/index.zh.md):

需要版本
Node.js^22.19 或 >= 24
pnpm11(通过 Corepack 启用)
API 密钥DeepSeek Platform 的 DEEPSEEK_API_KEY

依次执行:

git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
cd deepseek-harness
pnpm install
pnpm run build

在仓库根目录创建已被 Git 忽略的 .env,写入你的密钥:

DEEPSEEK_API_KEY=sk-your-key-here

然后验证环境就绪:

pnpm run dsh web

打开 http://127.0.0.1:3080——浏览器里出现 Web UI,说明你的开发环境已经能跑 DSH 了。

💡 插件开发教程假定你从已完成快速开始的仓库检出开始(来源:docs/user/develop/basic/index.zh.md)。也就是说:先能跑,再开发。后面所有命令都默认你在仓库根目录执行。


3. 最小插件代码:name、apply 与 inject

3.1 插件是什么:一个导出 apply 的模块

官方教程的定义(来源:docs/user/develop/basic/index.zh.md):

在 Harness 中,插件是一个导出 apply 函数的 TypeScript 模块。框架在加载时调用 apply,传入一个 ctx(上下文对象),你通过 ctx 注册能力。

最简插件长这样——这就是全部结构:

import type { Context } from 'cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // Register capabilities here.
}

(来源:docs/user/develop/basic/index.zh.md

三个要素,逐个拆开:

要素是什么一句话人话
name插件的名字,加载器诊断时用它标识插件「我是谁」
apply(ctx)框架加载时调用的效应函数「我贡献什么」——往 ctx 上注册能力
ctx上下文对象,插件和系统共享的「公共黑板」「我在哪儿、能碰什么」

📝 顺带一提:Cordis 教程指出 name 是可选的显示元数据,只用于诊断信息中标识插件(来源:docs/cordis-tutorial/01-first-plugin.zh.md)。但建议永远写上——插件多了之后,日志里能认出谁是谁很重要。

3.2 让插件真的「打招呼」:我们的 hello-plugin

照葫芦画瓢,写一个会打招呼的插件(来源:docs/user/develop/basic/index.zh.md):

import type { Context } from 'cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // Required dependencies are ready before apply runs.
  console.log('[hello-plugin] plugin loaded!')
}

这就是我们要的「Hello, DSH!」——框架加载插件时调用 applyconsole.log 就会在终端打印一行。你不需要写任何「启动框架」的代码;插件只描述自己的贡献,组合的事交给装配文件(这是 Cordis 教程第一章的原话,来源:docs/cordis-tutorial/01-first-plugin.zh.md)。

3.3 需要别人帮忙时:补一行 inject

如果你的插件要使用别的插件提供的能力(比如工具注册表 tools),就补一行 inject(来源:docs/user/develop/basic/index.zh.md):

import type { Context } from 'cordis'

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

export function apply(ctx: Context) {
  // ctx.tools is ready here.
  ctx.tools.register(/* ... */)
}

inject 的意思是「我依赖这些东西」——框架会保证这些依赖就绪之后才执行你的 apply。上一课我们学过:组件定义 = inject(依赖声明 d)+ apply(效应函数 e)两张图纸,框架用 ctx.use 把图纸实例化成带生命周期的 fiber;卸载时 fiber 的 dispose自动撤销插件注册的一切。你写的 apply 只负责「贡献什么」,生命周期完全交给框架。

💡 记住这个最小骨架:name + inject + apply。hello-plugin 只需要前两个;后面写工具插件时,injectctx.tools.register 就会登场。

3.4 一个真实包的物理结构:它放在哪儿

hello-plugin 写在一个临时目录里就够了。但如果要把插件变成仓库里的正式包,实操手册给了逐文件清单(来源:docs/cookbook/adding-a-package.zh.md):

packages/<group>/<pkg>/
  package.json     # 包名、依赖、构建产物入口
  tsconfig.json    # TypeScript 编译配置
  src/index.ts     # service 默认导出或插件(name/inject/apply/Config)
  README.md        # 服务 API、事件、扩展点、设计说明

其中 <group> 是纯容器分组(corellmbashsubagenttodoutil 等),package.json 有严格的不变式:private: truetype: modulemain: "lib/index.js"types: "lib/types/index.d.ts"cordis 同时出现在 peerDependencies 和 devDependencies 中。

3.5 真实生产插件长这样:agent-spine-demo

别被「最小」吓到——真实的 DSH 插件,本质就是同样的 name + apply,只是 apply 里干的事多。仓库里有个可运行的最小示例包 agent-spine-demo,它把一整个智能体主干(十几个子插件)作为组合包挂载起来(来源:packages/examples/agent-spine-demo/src/index.ts,已省略大部分子节点):

import type { Context } from 'cordis'
import Timer from '@cordisjs/plugin-timer'
import LlmService from '@deepseek-ai/dsh-llm'
// ...更多子插件 import...

export const name = 'agent-spine-demo'

export function apply(ctx: Context, config: Config): void {
  // ...
  ctx.plugin(Timer)
  ctx.plugin(LlmService)
  ctx.plugin(AgentRegistry)
  ctx.plugin(AgentLoop, { agents: config.agents ?? [] })
  // ...
}

注意两件事:一是它的 apply 拿到了第二个参数 config——插件可以用它接受用户配置;二是它通过 ctx.plugin(...) 把其他插件作为子节点挂载——插件可以嵌套插件,这正是「一切皆插件」的组合方式。它的 package.json 也印证了 3.4 节的不变式(来源:packages/examples/agent-spine-demo/package.json):

{
  "name": "@deepseek-ai/dsh-agent-spine-demo",
  "type": "module",
  "main": "lib/index.js",
  "types": "lib/types/index.d.ts",
  "peerDependencies": {
    "cordis": "^4.0.0-rc.7"
  }
}

所以,「写插件」与「写 hello-plugin」是同一件事,只是规模不同:结构永远是 name + inject + apply


4. 怎么加载它:cordis.yml、profile 与 dsh plugin add

4.1 本地开发:cordis.yml + --patch 覆盖层

插件文件写好了,怎么让它出现在运行中的 DSH 里?本地开发用的是装配文件 cordis.yml。在仓库根目录建一个临时项目:

mkdir -p scratch-plugin/src

把 hello-plugin 存成 scratch-plugin/src/my-plugin.ts,再创建 scratch-plugin/cordis.yml,用 insert 把它插进装配(来源:docs/user/develop/basic/index.zh.md):

- insert:
    - id: hello
      name: './src/my-plugin.ts'

id 是装配里的名字,name 指向插件模块——可以是相对路径,也可以是 npm 包名。然后带着这份覆盖层启动 Web UI:

pnpm run dsh web --patch ./scratch-plugin/cordis.yml

加载器(Loader)读取 cordis.yml、解析 ./src/my-plugin.ts、把它作为子插件挂载,然后 Cordis 调用你的 apply(ctx)(来源:docs/cordis-tutorial/01-first-plugin.zh.md)。打开 http://127.0.0.1:3080启动期间,终端会打印 [hello-plugin] plugin loaded!——这就是「Hello, DSH!」真正跑起来了。

📝 cordis.yml 里的各项会并发启动,所以列表位置不保证加载先后;真正的顺序由服务依赖(inject)决定,而不是文件里的位置(来源:docs/cordis-tutorial/01-first-plugin.zh.md)。

4.2 两种加载方式的分工

方式场景怎么用
cordis.yml + --patch本地开发、试自己的插件pnpm run dsh web --patch ./scratch-plugin/cordis.yml
profile(配置组合包)日常启动、组合多个插件dsh --profile <name> 按 manifest 顺序组合各 bundle 补丁层
dsh plugin add安装正式发布的插件包把包打包成 bundle,dsh plugin add your-package 装进 profile

前两种这一课就能用;第三种是「发布」那一课的事,这里先知道有这条路(来源:docs/user/develop/basic/publish.zh.md):打包成组合包package.json 里声明 dsh.bundle,指向一份 cordis.patch.yml),发布到 npm 或交付 tarball,别人执行 dsh plugin add 即可安装。dsh plugin --profile demo add . 会把本地 checkout 链接进 profile 并追加进 dsh.profile.bundlesdsh --profile demo --dump-config 可以先查看合成后的完整配置。

4.3 运行验证:加载即生效、卸载即还原

现在做两个验证,体验 DSH 最核心的承诺——「加载即生效、卸载即还原」(呼应第二章论文与第三章 fiber 机制):

  1. 加载即生效:带着 --patch 启动,终端立刻打印 [hello-plugin] plugin loaded!。不需要注册中心、不需要重启系统、不需要改任何现有代码——插件装上,能力就有了。
  2. 卸载即还原:把 cordis.yml 里的 insert 段落删掉(或去掉 --patch 参数)再启动——终端不再打印,一切回到插件存在之前的样子。

为什么能做到「卸载即还原」?官方教程的原话(来源:docs/user/develop/basic/index.zh.md):

通过 ctx 注册的任何东西——事件监听、工具、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。

这就是 fiber 的 dispose 在起作用:console.log 只是打个招呼,但换成注册工具、监听事件、挂定时器也一样——每一样都被记账,卸载时一次结清。只有少数需要手动管理的资源(比如一个网络连接)才用 ctx.effect() 告诉框架怎么清理。

🔁 呼应第二章与第三章:第二章说 Cordis 的「时空可组合性」承诺装上能拆下、拆下不留痕,第三章说这个承诺靠 fiber 的 dispose 实现——现在你亲手验证了它。DSH 敢让智能体「自指修改」、敢热更新插件,底气都在这里。


关键点回顾

  • 插件 = 导出 apply 函数的 TypeScript 模块name(我是谁)、inject(我需要什么,框架保证就绪后才执行)、apply(我贡献什么,往 ctx 上注册能力)。
  • 环境准备:克隆 deepseek-harness-sdk 仓库 → pnpm installpnpm run build.env 里配好 DEEPSEEK_API_KEYpnpm run dsh web 能开 Web UI。
  • 最小 hello-pluginexport const name = 'hello-plugin' + export function apply(ctx) { console.log('[hello-plugin] plugin loaded!') }——这就是全部。
  • 真实包结构packages/<group>/<pkg>/ 下的 package.jsontsconfig.jsonsrc/index.tsREADME.mdpackage.jsonmain / types / type: module / cordis 双依赖等不变式;真实例子见 agent-spine-demoapply 里用 ctx.plugin 组合子插件)。
  • 本地加载cordis.ymlinsert 一条(id + 指向源码的 name),用 pnpm run dsh web --patch ./scratch-plugin/cordis.yml 启动;正式安装走 bundle + dsh plugin add / profile。
  • 加载即生效、卸载即还原:通过 ctx 注册的一切在插件卸载时自动清理,你不需要手动 removeListener 或 clearInterval——这是 Cordis「装上能拆下」承诺的落地。

🚀 恭喜,你亲手写下了 DSH 里的第一个插件——虽然它只会打一声招呼。下一步让插件真正干活:写一个能被模型调用的工具,让「Hello」变成「我能帮你做事」。下一课见!

自测题 · 第一个插件

完成作答后点击「提交答案」,可以查看对错与解析。

1. 一个插件(组件)定义由哪两部分组成?
2. 关于 apply 函数和 ctx,下列说法正确的是?
3. 本地开发时,怎么把一个插件加载进运行中的 DSH?
4. 「加载即生效、卸载即还原」是什么意思?