第 6 章 插件开发实战:从 Hello 到第一个工具
这一章全程动手。目标:创建一个插件 → 加载进 Web UI → 再给它加一个能被模型调用的工具。整个流程在 15 分钟内可以走完。
6.1 准备:一个源码 checkout
插件开发建议从官方仓库的源码 checkout 开始(这样能用 pnpm dsh 和内置的 build 环境):
代码块
Plain Text
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
然后在仓库根目录建一个实验项目:
代码块
Plain Text
mkdir -p scratch-plugin/src
6.2 第一个插件:Hello
创建 scratch-plugin/src/my-plugin.ts:
代码块
Plain Text
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
这就是一个完整插件:导出 name 和 apply(ctx)。框架加载时调用 apply,把上下文 ctx 交给你。
6.3 加载它:--patch 覆盖层
创建 scratch-plugin/cordis.yml。注意插件路径必须是绝对路径,把 /absolute/path/to/deepseek-harness 换成你自己的:
代码块
Plain Text
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
用覆盖层启动 Web UI:
代码块
Plain Text
pnpm dsh web --patch ./scratch-plugin/cordis.yml
打开 http://127.0.0.1:3080,你应该在启动终端的日志里看到:
代码块
Plain Text
[hello-plugin] plugin loaded!
恭喜,你的第一个插件已经跑起来了。
6.4 插件的自动清理
通过 ctx 注册的任何东西——事件监听、工具、定时器——在插件卸载时都会被自动清理,你不用手动 removeListener 或 clearInterval。
如果需要手动清理的资源(比如一个网络连接),用 ctx.effect() 声明:
代码块
Plain Text
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
6.5 声明依赖:inject
如果插件要用其他服务(如工具注册表 tools、模型层 llm),声明 inject,框架会等服务就绪再加载:
代码块
Plain Text
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
6.6 开发一个工具:greet
把 scratch-plugin/src/my-plugin.ts 替换成下面这个"问候工具":
代码块
Plain Text
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
拆解一下这个 DSL:
•
parameters:声明工具入参,defineTool 会根据它推导并校验 args 的类型;
•
output.schema:声明工具返回值的规范形式;
•
output.render:把规范值转换成面向模型的内容;
•
execute:真正的执行逻辑。
重启开发命令(如果还在运行):
代码块
Plain Text
pnpm dsh web --patch ./scratch-plugin/cordis.yml
在 Web UI 里输入:
代码块
Plain Text
Use the greet tool to greet Ada.
模型会调用 greet,并收到 Hello, Ada! 这个工具结果——一个能被模型自主调用的工具就诞生了。
6.7 插件的三种形态
函数形式之外,还有对象形式和类形式:
代码块
Plain Text
// 对象形式
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
代码块
Plain Text
// 类形式:需要向其他插件提供服务时使用
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
大多数场景函数形式就够了;类形式意味着你的插件本身是一个服务,其他插件可以依赖它。
6.8 本章小结
•
插件 = 导出 apply(ctx) 的 TS 模块,通过 ctx 注册能力;
•
--patch 覆盖层是开发期加载插件的最快方式(路径要绝对路径);
•
ctx 的注册自动清理,ctx.effect() 处理手动资源;
•
inject 声明服务依赖,框架保证顺序;
•
defineTool 是写面向模型工具的标准姿势:入参 schema + 执行 + 渲染。
下一章,让插件学会"接受配置"。