在开始任何技术学习之前,最令人兴奋的时刻莫过于亲眼看到自己的第一行代码跑起来。本章将带你完成这个里程碑——创建一个最小的 Harness 插件,并把它加载到 Web UI 中。你不需要理解太多复杂的概念,只需要跟着步骤一步步操作,就能在终端里看到插件成功加载的日志输出。
在这个过程中,你会接触到 Harness 插件最核心的三个概念:插件是什么、如何注册、以及框架如何管理插件的生命周期。读完本章,你不仅拥有一个可以运行的插件,更重要的是,你将对 Harness 的插件机制建立起直观的认知。
准备工作
在开始之前,请确保你已经完成了DeepSeek Harness 从源码运行 的准备工作。简单来说,就是你已经把 DeepSeek Harness 的仓库克隆到了本地,并且能够成功运行 pnpm dsh 命令。
如果你还没有完成这一步,建议先按照官方 README 的指引操作。本章的所有操作都基于这个源码仓库的环境。
创建本地项目
首先,我们需要在仓库根目录创建一个临时项目,用来存放本章的插件代码:
mkdir -p scratch-plugin/src这个命令会在仓库根目录下创建 scratch-plugin/src 文件夹。scratch-plugin 是我们整个插件的项目目录,src 用于存放 TypeScript 源文件。
为什么叫 scratch? “Scratch” 在编程中常指“草稿”或“临时”的意思。我们用它来命名,表示这是一个用于学习和实验的临时项目——你可以随时删除它,而不会影响 Harness 本身的代码。
插件是什么?
在动手写代码之前,先理解一个核心问题:Harness 插件本质上是什么?
在 Harness(以及它底层的 Cordis 框架)中,一个插件就是一个导出了 apply 函数的 TypeScript 模块。当框架启动时,它会加载这个模块并调用 apply 函数,同时传入一个 ctx(上下文对象)作为参数。你通过这个 ctx 来向框架注册各种能力——比如工具、事件监听器、定时任务等。
最简单的插件长这样:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里注册各种能力
}这就是一个完整插件的最小结构。是的,就这么简单——一个 name 和一个 apply 函数,足矣。
创建插件文件
现在,让我们动手创建第一个真正的插件文件。在 scratch-plugin/src/ 目录下创建 my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// 当插件加载时,框架会调用这个函数
console.log('[hello-plugin] plugin loaded!')
}这段代码做了三件事:
导入 Context 类型:从 Cordis 框架中引入上下文对象的类型定义。注意这里用了 import type,这表示我们只在 TypeScript 类型层面使用它,不会打包进最终的 JavaScript 代码——这是一种更轻量的导入方式。
导出插件名称:export const name = 'hello-plugin' 给插件起了一个唯一标识名。这个名字会在框架的日志、依赖管理和热更新中用到。
导出 apply 函数:这是插件的入口点。当框架加载你的插件时,会调用这个函数。目前我们只做了一件事——在控制台打印一条日志,证明插件确实被加载了。
给初中级开发者的提示:console.log 是最直接的调试手段。在插件开发初期,通过日志来确认加载顺序、参数值、执行流程,是非常实用且高效的调试方式。
实操细节
在 scrath-plugin 目录下创建 tsconfig.json,添加内容:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": false,
"noEmit": true
},
"include": ["src/**/*.ts", "*.ts"]
}可消除代码编辑器中"Cannot find module…"的提示报错。
注册到 cordis.yml
插件文件写好了,但框架还不知道它的存在。我们需要通过一个配置文件告诉框架:“嘿,这里有个插件,启动的时候记得加载它!”
在 scratch-plugin/ 目录下创建 cordis.yml 文件:
- insert:
- id: hello
name: 'file:///E:/ProjectsAI/deepseek-harness/scratch-plugin/src/my-plugin.ts'这里有三个关键点需要注意:
第一,insert 表示我们要往框架的插件列表中“插入”一个新的插件。id: hello 是这个插件在配置层面的标识,可以和插件内部导出的 name 不同——id 是配置层的称呼,name 是代码层的标识。
第二,name 字段的值必须是绝对路径,且必须加上 file:// 协议(笔者测试环境为window)。
第三,这个 cordis.yml 文件是一个“补丁文件”(patch file)。它不会替换框架原有的配置,而是在原有配置的基础上“打补丁”——追加我们新写的插件。这种设计让你可以在不修改框架核心配置的情况下,灵活地加载本地开发中的插件。
常见错误:新手最容易犯的错误就是忘记使用绝对路径,或者路径写错了。如果启动后看不到插件的日志,请先检查路径是否正确。
启动 Web UI
一切准备就绪,现在来见证奇迹的时刻。
在仓库根目录下运行以下命令:
pnpm dsh web --patch ./scratch-plugin/cordis.yml这个命令的含义是:启动 Harness 的 Web UI 服务,同时通过 --patch 参数加载我们刚才写的补丁配置文件。
启动成功后,你会看到终端输出一系列日志信息。在其中,你应该能找到这样一行:
[hello-plugin] plugin loaded!恭喜你!到此为止,你的第一个 Harness 插件已经成功加载并运行了。
此时,打开浏览器访问 http://127.0.0.1:3080,你会看到 Harness 的 Web 界面。虽然我们的插件目前没有在界面上添加任何可见的元素,但它已经在后台默默地完成了加载——这就是一个最小插件所能做到的事情。
自动清理:一个值得信赖的“管家”
现在插件能跑了,但还有一个重要的问题:当插件被卸载时,我们注册的那些能力怎么办?
在很多框架中,你需要手动清理——移除事件监听、清除定时器、关闭网络连接……稍有不慎就会造成内存泄漏或残留的副作用。
但 Harness 的设计让你可以省去这些烦恼。通过 ctx 注册的任何东西——事件监听、工具、定时器——在插件卸载时都会被自动清理。你不需要手动调用 removeListener 或 clearInterval。
这背后的原理是:Cordis 框架会追踪所有通过 ctx 注册的资源,并在插件卸载时统一清理。这就像有一个尽职的管家,你离开房间时他会帮你关灯、锁门,你完全不用操心。
但如果你有需要手动清理的资源(比如一个数据库连接池、一个 WebSocket 连接),框架也提供了明确的清理入口——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)
})
}ctx.effect() 接收一个函数,这个函数可以返回一个清理函数。当插件卸载时,框架会自动调用这个清理函数,让你有机会释放手动管理的资源。这个机制确保了你的插件在任何情况下(正常退出、热更新、甚至加载失败后的回滚)都能正确地清理自己。
声明依赖:让插件按正确的顺序加载
随着插件功能的丰富,你可能会需要用到其他服务——比如 tool(工具注册表)、llm(大语言模型服务)等。
如果你的插件依赖这些服务,就需要声明 inject 数组:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// 此时 ctx.tools 已经就绪,可以安全使用
ctx.tools.register(/* ... */)
}声明了 inject=['tools'] 之后,框架会确保 tools 服务已经就绪才会加载你的插件。如果 tools 服务加载失败,你的插件也不会被加载——这避免了“依赖缺失”导致的运行时错误。
给初中级开发者的提示:依赖声明是一种“契约”。你告诉框架“我需要什么”,框架保证“在你开始工作之前,这些东西都准备好了”。这种设计让插件之间的协作变得清晰而可靠。
插件的三种形态
到目前为止,我们使用的都是函数形式的插件——直接导出一个 apply 函数。这是最常用、最简洁的方式。
但 Harness 还支持另外两种形态:
对象形式
import type { Context } from '@deepseek-ai/cordis'
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}对象形式其实就是把函数形式的各个导出项(name、inject、apply)整合到一个对象里,通过 export default 导出。这种写法在需要默认导出而非命名导出时比较方便。
类形式
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
// 在构造函数中执行同步初始化
}
}类形式最为强大——当你的插件需要向其他插件提供服务时,使用类形式。通过继承 Service 类,你的插件本身就成了一个可被其他插件依赖的服务。
给初中级开发者的建议:在刚开始学习时,优先使用函数形式即可。它足够简单,也足够强大。等你需要构建更复杂的插件体系时,再逐步探索对象形式和类形式。
总结
回顾一下本章我们完成的内容:
步骤 | 做了什么 | 核心知识点 |
|---|---|---|
创建项目 | mkdir -p scratch-plugin/src | 插件代码放在独立的目录中 |
编写插件 | 导出 name 和 apply 函数 | 插件是导出 apply 的模块 |
注册插件 | 创建 cordis.yml 补丁文件 | 通过 --patch 加载本地插件 |
启动验证 | pnpm dsh web --patch 补丁文件 | 日志中看到 plugin loaded! |
你还接触到了三个重要的框架机制:
自动清理:通过 ctx 注册的资源会自动清理,无需手动管理。
ctx.effect():为需要手动清理的资源提供明确的清理入口。
依赖声明:通过 inject 确保依赖服务就绪后再加载插件。
你已经成功写出了第一个 Harness 插件,并且理解了插件加载的完整流程。这是一个很好的开始——接下来的章节中,我们会在这个基础上让插件变得越来越“有本事”:给它添加工具、让它接受配置、最终把它打包成一个可分发的组合包。
下一章,我们将学习如何为你的插件添加能够被大语言模型调用的工具能力。