DeepSeek-Harness 插件底层是如何工作的
面向开发者的 dsh 插件入门:apply(ctx, config) 契约、三种写法、package.json 里的 dsh 字段,以及如何让插件被收录。
一个 DeepSeek-Harness(dsh)插件,就是一个导出了 apply(ctx, config) 函数的普通 JavaScript 或 TypeScript 模块。这就是全部契约——没有独立的 manifest 文件来描述一个插件是什么、做什么。分发所需的元信息写在插件 package.json 里的 dsh 字段里,而不是一份独立的 schema 文件。本文梳理这套契约、三种插件写法的区别、一个插件如何被打包成可以 dsh plugin add 的形态,以及它最终如何被人发现。
契约:apply(ctx, config)
最简单的插件长这样:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里通过 ctx 注册能力
}
ctx 是插件加载时拿到的 Cordis 上下文。你通过它注册的一切——事件监听、ctx.tools.register() 调用、ctx.effect() 清理逻辑——在插件卸载时都会被框架自动清理。常见场景下你不需要手写 dispose 逻辑。
一个插件还可以选择性地导出:
inject——一个字符串数组,声明它依赖的服务(例如['tools', 'llm'])。框架会等这些服务就绪后才调用apply,依赖的服务消失时也会自动卸载这个插件。Config——一份 Schemastery schema,描述插件可配置的用户选项,dsh 加载插件时会用它做校验并填充默认值。
三种写法
函数式(如上)是默认写法,覆盖了绝大多数场景——一个 apply 函数就够了。
对象式把同样的内容包进一个默认导出:
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx) {
// ...
},
}
类式,继承自 Service,用于这个插件本身要向其他插件暴露一个服务的场景:
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService') // 挂载为 ctx.myService
}
}
一旦挂载完成,任何其他插件都可以声明 inject: ['myService'] 来拿到 ctx.myService——不同作者写的插件之间,正是靠这种方式互相组合。
dsh 的每一个功能,本质都只是插件的不同形态
这正是 dsh 架构清晰易懂的地方:工具、命令、技能、MCP server 之间没有各自独立的注册系统。它们全都是同一套 apply(ctx, config) 机制,只是指向了不同的扩展点:
| 能力 | 实现方式 |
|---|---|
| 工具 | ctx.tools.register()——其 schema 会自动进入 prompt 组装流程 |
命令(/xxx) | ctx.commands——仅作用于界面,不会产生模型消息 |
| 技能 | 注册一段 prompt 内容加一个工具;被调用时把技能内容注入进去 |
| MCP server | 每个 server 对应一个插件:发现其工具后逐个 ctx.tools.register() |
| 钩子(Hook) | 监听 agent/session-start、agent/pre-step、tools/pre-execute 等生命周期扩展点 |
| LLM 适配器 | 通过 ctx.llm.registerAdapter() 注册一个 LlmAdapter 子类 |
| UI 扩展 | 监听 session/event,或向内置 Web 客户端注册一个 ConversationNodeDefinition |
| 权限 / 沙箱 | 从 tools/pre-execute 返回 allow / deny / ask,或提供自定义的 ctx.sandbox 后端 |
| 后台 / 定时任务 | 注册进 ctx.jobs,触发 followup(..., { source: { kind: 'cron' } }) |
| 子代理(Subagent)委派 | 注册进 ctx.subagents 的 provider registry |
实际来看,这意味着如果你已经会写一个工具插件,那你就已经掌握了写钩子、命令或 MCP 桥接插件的大部分知识——形态是一样的,只是指向了 ctx 上不同的位置。
打包:package.json 里的 dsh 字段
一个插件的 package.json 不需要一份专门定制的 manifest——它需要的是一个 dsh.bundle.patch 字段:
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" }
}
}
dsh.bundle.patch 指向一个 YAML 文件——一个 patch(补丁)——描述这个包真正往正在运行的 Cordis 插件树里插入了什么:
- insert:
- id: hello
name: '/absolute/or/module/path/to/my-plugin.ts'
config:
someOption: true
每一项要么 insert 一批新的插件行(带上 id、指向模块的 name,以及可选的 config),要么按 id 定位、覆盖某一行已有的 config。要注意的是:一个 patch 会整体替换目标行的 config,而不是深度合并——如果你只想覆盖其中一个字段,也需要把其余字段原样重新写一遍。
还有一个相关但独立的概念值得了解:profile(也就是传给 dsh --profile <name> 的那个东西)本身也是靠一个 dsh.profile.bundles 字段来描述的——一个有序列表,列出这个 profile 由哪些 bundle 包组成,@deepseek-ai/dsh-base 永远排在第一个。当你执行 dsh plugin --profile <name> add <包> 时,dsh 会通过 pnpm 安装这个包,如果它声明了 dsh.bundle,就会自动把它追加进这个列表——你不需要手动编辑它。
一个典型的仓库目录结构
把上面这些拼在一起,一个真实可安装的插件仓库通常长这样:
hello-plugin/
├── package.json # 声明 dsh.bundle.patch
├── cordis.patch.yml # 这个 bundle 要 insert/覆盖 哪些配置行
├── index.js # 入口文件——导出 apply / name / inject / Config
├── src/ # 若为 TypeScript 源码,构建产物放到 lib/ 下
└── README.md
如果你通过 GitHub 而不是预构建的 npm 包来分发源码,记得加一个 prepare 脚本,让 pnpm 在安装时自动把 src/ 编译到 lib/——这个脚本在什么情况下需要用户显式授予 allowBuilds 权限、以及这道安全提示存在的原因,可以参考《我们的安装指南》。
让你的插件被发现
DeepSeek AI 没有运营插件市场,所以官方这边并不存在一个提交表单。真正有效的是这两件事:
- 给你的仓库打上 GitHub topic
dsh-plugin。 这是官方文档里明确点名的唯一发现机制,也是各类基于搜索的工具和爬虫首先会去找的信号。 - 提交到社区索引里。
awesome-dsh-plugin/awesome-dsh-plugin是目前维护最活跃的社区清单——目前收录 365 款插件、覆盖 11 个分类——它接受按既定的- [owner/repo](url) - 一句话描述格式提交 PR 新增条目。
FindHarness 自己的 /plugins 目录正是基于同一份社区精选数据构建的,并叠加了实时抓取的 GitHub 元数据(star 数、许可证、最近推送时间、完整 README)。想被我们收录,意味着要先被 awesome-dsh-plugin 收录,并保持你仓库的 topic 和元数据准确——我们这边没有额外的单独提交流程。
想了解安装这一侧的完整视角——用户实际会敲下什么命令来拉取你的插件,以及如果它需要构建步骤时用户会看到什么安全提示——可以读《如何安装 DeepSeek-Harness 插件》。如果你想先感受一下"好插件"长什么样,《2026 年 10 款最佳 DeepSeek-Harness 插件》是一份按真实 star 数排名的真实样本。