作者:Roson
引用原文链接:https://mp.weixin.qq.com/s/kLbIOlhAkU7K3X-SjHQ4rg

最近上手了 DeepSeek 开源的 DeepSeek Harness(dsh)——一个“万物皆插件”的 Agent 运行时框架。模型适配器、工具注册表、会话日志,甚至 Agent 主循环本身,全都是可替换的插件。

这篇文章是我亲手做一个插件并发布到 GitHub 的完整复盘:从 5 分钟跑通第一个插件,到踩过的两个真实的坑,再到配置化、打包、发布。全程实操,命令可复制。


一、第一个 Harness 插件

我们从创建一个最小的 Harness 插件开始,并将其加载到 Web UI 中。

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

deepseek-harness 仓库根目录创建本教程使用的临时项目:

bash
mkdir -p scratch-plugin/src

目录结构如下:

cmd
deepseek-harness/
└── scratch-plugin/
    ├── src/
    │   └── my-plugin.ts
    └── cordis.yml

创建两个文件:

scratch-plugin/src/my-plugin.ts

typescript
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',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true },
    },
    output: {
      schema: { type: 'string' },
      render: (_a, v) => [{ type: 'text', text: v }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

scratch-plugin/cordis.yml

注意:这里需要使用插件文件的绝对路径,如果是Window系统,还需要在开头添加 file:/// 前缀。

yaml
- insert:
    - id: hello
      name: '/绝对路径/scratch-plugin/src/my-plugin.ts'

重新启动 Web UI:

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

在浏览器中打开 http://127.0.0.1:3080/,对 Agent 说一句:

Use the greet tool to greet Ada

模型就会调用你写的工具,返回:

text
Hello, Ada!

到这里,恭喜你,已经可以开发第一个插件了 🎉🎉🎉


二、插件配置:告别硬编码

本节内容继承上一节。上一节的 execute 函数中:

typescript
return `Hello, ${args.name}!`

打招呼方式被写死了,比如使用 Hello。如果想改成“你好”,就必须修改代码。能不能直接在配置文件中修改呢?

dsh 有条铁律:凡是两个部署可能想要不同的值,必须是配置字段。 这也是 Harness 规范的要求。

只需改动两个步骤:

  1. 在插件中导出一个 Config 类型和同名的 Schemastery schema;默认值直接写在 schema 中。
  2. scratch-plugin/cordis.yml 新插入的本地插件行中添加配置。

改动后的代码如下。

scratch-plugin/src/my-plugin.ts

typescript
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export interface Config {
  greeting: string
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
})

export function apply(ctx: Context, config: Config) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: {
        type: 'string',
        required: true,
        description: 'The name to greet',
      },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `${config.greeting}, ${args.name}!`
    },
  }))
}

scratch-plugin/cordis.yml

注意:这里需要使用插件文件的绝对路径。

yaml
- insert:
    - id: hello
      name: '/绝对路径/src/my-plugin.ts'
      config:
        greeting: '你好'

加载时会进行 schema 配置校验,类型错误会直接报错拒载(fail loud),缺省值则会自动填充。

重启 Web UI 后,同样的提问会返回:

text
你好, Ada!

三、打包:从源码到可安装 Bundle

要让别人通过 dsh plugin add 一键安装,插件需要变成带有 dsh.bundle 声明的 npm 包。

最终目录结构如下:

text
dsh-greet-plugin/
├── package.json(dsh.bundle 声明)
├── tsdown.config.ts(打包配置)
├── cordis.patch.yml(发布版配置层)
├── src/index.ts(插件源码)
└── lib/(构建产物,要提交进仓库!)

下面是具体打包步骤。

Step 0:创建项目目录

bash
mkdir -p dsh-greet-plugin/src
cd dsh-greet-plugin

Step 1:创建 4 个核心文件

文件 1:package.json

json
{
  "name": "dsh-greet-plugin",
  "version": "0.1.0",
  "description": "A greet tool plugin for DeepSeek Harness (dsh).",
  "license": "MIT",
  "type": "module",
  "main": "lib/index.mjs",
  "types": "lib/index.d.mts",
  "files": ["lib", "cordis.patch.yml"],
  "scripts": {
    "build": "tsdown",
    "typecheck": "tsc --noEmit",
    "check": "pnpm typecheck && pnpm build"
  },
  "peerDependencies": {
    "@deepseek-ai/cordis": "*"
  },
  "devDependencies": {
    "@deepseek-ai/cordis": "link:../deepseek-harness/vendor/cordis",
    "@deepseek-ai/dsh-tools": "link:../deepseek-harness/packages/core/tools",
    "@deepseek-ai/schemastery": "link:../deepseek-harness/vendor/schemastery",
    "@types/node": "^22.10.0",
    "tsdown": "^0.22.2",
    "typescript": "^5.9.0"
  },
  "dsh": {
    "bundle": {
      "patch": "./cordis.patch.yml"
    }
  }
}

文件 2:tsdown.config.ts

这是打包策略的核心配置:

typescript
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/index.ts'],
  outDir: 'lib',
  format: 'esm',
  dts: true,
  // 只 external cordis(宿主必有),dsh-tools/schemastery 打进 bundle,
  // git 分发零 registry 依赖
  external: ['@deepseek-ai/cordis'],
})

文件 3:cordis.patch.yml

发布版中的 name 使用包名,不再使用绝对路径:

yaml
- insert:
    - id: greet-plugin
      name: dsh-greet-plugin
      config:
        greeting: 'Hello'

文件 4:src/index.ts

把原来的 src/my-plugin.ts 原样复制为:

text
src/index.ts

Step 2:安装依赖

bash
pnpm install

Step 3:构建

bash
pnpm check

构建产物为:

text
lib/index.mjs
lib/index.d.mts

划重点: lib/ 构建产物要提交进仓库,.gitignore 千万不要忽略它。用户通过 Git 安装时拿到的是现成产物,不需要运行构建脚本,也就不需要 allowBuilds 授权,安装体验最顺滑。


五、发布到 GitHub

先在 GitHub 建好空仓库,不要生成 README 文件,否则后续 push 时可能产生冲突。

然后在本地项目目录中执行以下命令:

bash
git init -b main
git add . && git commit -m "v0.1.0"
git tag v0.1.0
git remote add origin git@github.com:你/dsh-greet-plugin.git
git push -u origin main --tags

打 tag 的意义是:用户可以 pin 住版本安装,实现可复现部署。

最后一步只能在网页上完成:进入仓库的 About → Topics,添加 dsh-plugin。这样社区就可以在 GitHub topic 页面发现你的插件。

登录并查看 Topic 页面:

https://github.com/topics/dsh-plugin

搜索:dsh-greet-plugin


六、安装你的插件

🎉 至此,任何人都可以这样安装你的插件。

新插件的 GitHub 地址:

text
git@github.com:luxiu666/dsh-greet-plugin.git

执行下面的安装命令:

bash
dsh plugin --profile web add github:luxiu666/dsh-greet-plugin#v0.1.0

重启 Web UI:

bash
pnpm dsh web

再次提问后,可以看到 Agent 已经调用刚才新安装的插件。

page-8-image-1.png


写在最后

dsh 还在 developer preview 阶段,正是入场的好时机——工具、LLM 适配器、沙箱后端、Web 界面节点,每一个能力接缝都留给社区插件。