DeepSeek Harness 写第一个插件:给 Agent 加个 IP 查询工具
生命不息,折腾不止。上次说好的「写插件」来了——今天手把手给 dsh 的 Agent 造一个 IP 查询工具,让它多一门手艺。
从基础安装到接中转站,dsh 已经跑起来了。但光会用别人的东西,那只是「用户」;能往里加自己的东西,才叫「玩家」。dsh 最迷人的地方就在这:一切皆插件,工具、模型、会话、UI 全都可以自己换。今天就兑现承诺,写第一个插件。
一、先搞懂:dsh 的「一切皆插件」到底是啥
DeepSeek Harness(简称 dsh)的核心设计就一句话:一切皆插件。模型适配器、工具、会话存储、Agent 循环、甚至 Web UI 的面板,全都是插件。底层由 Cordis 这个元框架负责加载、卸载和依赖解析,插件之间通过「服务」和「事件」协作。
所以插件不是外挂,它可以替换或扩展 Agent 的任何部分。理解了这个,你就摸到了 dsh 的命门。
那一个插件到底长啥样?官方文档(仓库里的 docs/user/develop/basic/index.zh.md)说得很直白:
插件是一个导出
apply函数的 TypeScript 模块。框架在加载时调用apply,传入一个ctx(上下文对象),你通过ctx注册能力。
最简单的插件,就这么几行:
1 | import type { Context } from '@deepseek-ai/cordis' |
几个要点记一下:
name:插件名,要唯一apply(ctx):入口函数,ctx 是「上下文对象」,注册工具、监听事件、开定时器都通过它inject:声明依赖,比如export const inject = ['tools'],框架会等工具注册表就绪后才调用你的 apply
还有两个很贴心的设计:
- 自动清理:通过 ctx 注册的事件监听、工具、定时器,插件卸载时全部自动清理,你永远不用手写 removeListener
- ctx.effect():如果你有网络连接这种要手动关闭的资源,用
ctx.effect(() => { ...; return () => 清理 })告诉框架怎么收尾
插件有三种写法:函数(日常够用)、对象(带生命周期钩子)、类(Service 子类,给别人提供服务)。写工具插件,函数形式基本就够了。
二、最小闭环:先让 dsh 认你这个插件
环境要求:Node ^22.19 或 >=24,低于这个版本 dsh 起不来。还没装 dsh 的先把 Web 版跑起来:
1 | npx @deepseek-ai/dsh web |
然后建个本地开发目录,写一个最朴素的插件:
1 | mkdir -p scratch-plugin/src |
1 | // scratch-plugin/src/hello-plugin.ts |
再建一个覆盖层文件 cordis.yml。注意:本地开发阶段,插件路径要写绝对路径,相对路径会解析失败:
1 | # scratch-plugin/cordis.yml |
用覆盖层启动:
1 | npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml |
启动日志里看到 [hello-plugin] plugin loaded!,说明 dsh 已经认你了。最小闭环达成:加载插件 → 注册能力 → 生效。
三、实战:写一个 IP 归属地查询工具
插件本身没意思,给 Agent 加个真能干的工具才有意思。今天的目标:让 dsh 里的 Agent 学会查 IP 归属地——你说「帮我看看 8.8.8.8 是哪的」,它调工具给你返回「美国 弗吉尼亚州 Ashburn,运营商 Google LLC」。
我用纯 ESM 写(免构建,Node 直接跑),接口用 ip-api.com 的免费 JSON 接口,不用注册不用 key,自用完全够(免费版每分钟约 45 次限额,商用要付费,以官网为准)。
1 | // index.js |
代码不长,拆开讲:
defineTool从@deepseek-ai/dsh-tools导入,帮你做参数校验和输出校验parameters:声明参数 schema,模型会照着这个自动生成调用参数execute(args):真正干活的地方,这里就是调 ip-api.com 的 HTTP 接口output.schema:返回值校验;output.render:把结构化结果渲染成模型能直接读的话- 坑:object 类型的输出 schema 必须写
additionalProperties: true,不然注册直接失败——我在这上面栽过
装进 profile 跑起来(两种方式二选一):
1 | # 方式一:本地路径直接装(需要 pnpm,没有就先 npm install -g pnpm) |
然后在 Web UI 里开个新会话,直接说:
帮我查一下 8.8.8.8 的归属地
你会看到 Agent 展开工具调用:IN 传参、OUT 返回结果,一气呵成。至此「加载插件 → 注册工具 → 模型调用 → 返回结果」的完整闭环就通了。
顺带说一句:不想手敲这些代码的话,可以把插件开发文档扔给 DeepSeek 让它自己写——社区里已经有人这么干出了 arXiv 搜索插件。模型写插件、你负责验收,这就是 Agent 时代的生产方式。dsh 默认接 DeepSeek 官方,习惯走中转的朋友也可以在 provider 里配 ai.aklibk.com,基础接入方式前面那篇讲过了,不重复。
四、从本地到发布:三件套打包
本地能跑只是第一步,想让插件真正可安装、可分享,需要三件套:
1. package.json(声明这是个 dsh 插件)
1 | { |
2. index.js——就是上面那个插件本体
3. cordis.patch.yml(告诉 dsh 怎么挂载它)
1 | - insert: |
⚠️ files 数组里必须包含 cordis.patch.yml!漏了它,包能装上但插件层根本不生效——这是发布目录里最常见的翻车点,装完发现啥都没有,先查这个。
发布三步走:
- 推到 GitHub 公开仓库
- 仓库打上
dsh-plugintopic——这是官方约定的发现机制,社区目录就靠它索引 - README 里写清安装方式、插件能访问什么、测试过的 dsh 版本
之后别人一条命令就能装:
1 | dsh plugin --profile web add github:你的账号/dsh-ip-lookup |
装完重启 profile 就生效。如果插件装了但界面/工具没出现,别急着猜,先看配置树:
1 | dsh --profile web --dump-config |
它会打印当前真正组合出的插件树,你的插件在不在里面一目了然——不在就是安装环节的问题,不是代码问题。
五、踩坑清单 & 安全提醒(必看)
把这一路的坑汇总一下:
- object 输出 schema 要写
additionalProperties: true,否则 defineTool 注册直接失败 - files 漏掉 cordis.patch.yml → 装上了但没挂载层,等于白装
- patch 是整行替换,不是深合并:改配置要重述整行 config,只写想改的那个字段会丢掉其它配置
- 插件是可信代码:dsh 插件跑在宿主进程里,能碰你的文件、网络、浏览器、终端。装社区插件前先看 README 和安装脚本,最好固定 tag/commit,再单独开个 profile 试装,别拿主力工作区当试验场
- 本地开发挂本地路径时用绝对路径,相对路径会解析失败
写完第一个插件,dsh 在你手里就不再是「开箱即用」的工具,而是「随你捏」的工作台了。
生命不息,折腾不止。下一篇咱们玩个大的:把一个任务拆给多个 Agent 并行干——多 Agent 协作实战,看看 dsh 怎么让几个「分身」同时开跑、最后汇总结论。