ByteNoteByteNote
写第一个 DeepSeek Harness 插件
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

写第一个 DeepSeek Harness 插件

API中转
¥120

DeepSeek Harness 的官方文档里,插件开发的第一课不是讲概念,而是带你用两个文件跑通一个会被真实加载的最小插件:一个导出 apply 函数的 TypeScript 模块,加一段 cordis.yml 覆盖层配置。本文整理自这份用户指南的入门篇,前置条件只有一个:本地有一份按官方 README 完成从源码构建与启动的 deepseek-harness 仓库检出。

第一个 Harness 插件的加载路径:两个文件经 patch 覆盖层挂进 Web UI

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

在 Harness 中,插件就是一个导出 apply 函数的 TypeScript 模块。框架在加载时调用 apply,传入一个 ctx 上下文对象,你通过它向框架注册能力。最小形态长这样:

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

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

这就是完整配置。教程给它起名 hello-plugin,全部逻辑只有一行日志:

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

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

留意 apply 里那句注释:必需的依赖会在 apply 执行之前就绪。这不是巧合而是框架的保证,后面声明依赖一节会再回到这一点。

挂进 Web UI:cordis.yml 覆盖层

先在仓库根目录建一个临时项目目录:

sh
mkdir -p scratch-plugin/src

把上面的插件代码存成 scratch-plugin/src/my-plugin.ts,再创建 scratch-plugin/cordis.yml,作为插入本地插件的 Web 覆盖层:

yaml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

把 name 字段里的路径换成你在仓库根目录执行 pwd 得到的绝对路径。这份配置有两个容易踩坑的细节:插件路径必须是绝对路径;patch 文件只贡献配置,不会改变 loader 解析模块路径时使用的 profile 目录。

接着带着覆盖层启动 Web UI:

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

打开 http://127.0.0.1:3080,启动期间终端会打印 [hello-plugin] plugin loaded!。看到这行输出,说明第一个插件已经在真实运行的 Harness 进程里挂载成功。

插件生命周期三件事:inject 声明依赖,apply 注册能力,effect 负责清理

自动清理:卸载不用善后

官方文档专门强调了这个机制:通过 ctx 注册的任何东西,包括事件监听、工具、定时器,在插件卸载时都会被自动清理,你不需要手动 removeListener 或 clearInterval。对于确实要手动收拾的资源,比如一个网络连接,用 ctx.effect() 告诉框架怎么清理:

ts
ctx.effect(() => {
  const timer = setInterval(() => {
    console.log('heartbeat')
  }, 5000)

  // The returned function runs when the plugin unloads.
  return () => clearInterval(timer)
})

规则只有一条:effect 工厂返回的那个函数,会在插件卸载时执行。把清理逻辑写进这个返回函数,剩下的交给框架。

声明依赖:inject

如果插件需要使用其他服务,比如 tools 或 llm,需要声明 inject:

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

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

框架会确保依赖的服务全部就绪之后,才加载你的插件。这正是前文那句注释的出处:apply 跑起来的时候,ctx.tools 已经是一个可以直接用的现成服务。

插件的三种形态

除了函数形式,插件还支持对象形式和类形式。对象形式把 name、inject、apply 组织成一个默认导出的对象;类形式继承 Service,静态 inject 字段声明依赖,构造函数里做同步初始化:

ts
import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // Perform synchronous initialization in the constructor.
  }
}

官方的建议很克制:大多数情况下,函数形式就足够了;当插件需要向其他插件提供服务时,再改用类形式,服务与依赖的完整机制在框架文档的服务章节里单独展开。

写在最后

把整条路径串起来:两个文件,一个最小 apply 模块加一段 patch 配置,一条 pnpm dsh web --patch 命令,插件就挂进了真实运行的 Web UI,而且从加载到卸载全程不用操心资源善后。按官方文档的指引,下一步可以了解工具定义的 DSL,给插件写一个能注册进 ctx.tools 的模型工具,或者学习插件配置,让插件接受用户自己的配置项。想绕开 Harness 直接吃透底层框架,官方另有 Cordis 框架教程,在临时目录里动手构建,全程无需 API 密钥。

相关文章

分享: