DSH plugin 怎么写工具插件:defineTool 参数与输出声明、execute 契约与 UI 卡片渲染
DSH plugin 写工具插件的固定套路是三步:声明 inject: ['tools']、在 apply 里 ctx.tools.register(defineTool({ ... }))、在 defineTool 里分别声明 parameters(模型能传什么)、output.schema(execute 必须返回什么)与 execute(怎么执行)。 无论你叫它 DSH插件 还是 DeepSeek插件,这套契约完全一样。
DSH plugin 工具插件的最小形态
一个工具插件 = 声明依赖 + 注册工具定义,注册是 effect 式的,插件卸载即自动注销。 官方给出的最小写法如下(来源):
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute path' },
limit: { type: 'number' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
},
}))
}
inject: ['tools'] 不能省——它让 Cordis 先备好工具注册表,apply 里才敢直接用 ctx.tools。依赖声明的完整机制见 怎么写插件。
DSH plugin 的 parameters 与 output:一个进、一个出
parameters 管入参、output.schema 管出参,两者职责不重叠。 官方对这条边界的表述是:defineTool 从 parameters 推导并校验 execute 的 args;execute 返回的是 output.schema 声明的那个 canonical 值,再由 output.render 转成模型可见内容(来源)。
- 参数自动校验:类型、必填键、字面量约束、精确其一联合、嵌套值都由框架在
execute之前验完——你不必再写类型判断,但 DSL 表达不了的约束(非空字符串、正数、跨字段规则)要自己查。 description是写给模型看的:它决定模型何时调用这个工具,不是注释。- 输出只声明一个根值:可以是对象、数组、标量或 null——按「这个值的诚实形态」来选,不要为了界面好看把文案塞进 schema。
DSH plugin 的 execute 契约:四条硬规则
execute 是一份契约,不是普通函数——四条规则越界就会出问题。
- 只返回一个 canonical 值。 不要返回内容块、不要让调用方从散文里解析 id 或字段;注册表会把它快照为无损 JSON、校验、冻结。
- 抛异常即
isError。 基础设施故障(文件不存在、网络断)直接 throw;业务上的非理想结果仍返回 canonical 值,例如「进程非零退出」应当是一个正常返回值,由渲染器解释,而不是异常。 - 遵守
exec.signal。 信号触发时取消在途工作——exec还携带不可变的执行身份与 token,args应视为只读输入。 - 注册后不要改自己的定义。 同进程的类型化贡献不是序列化边界,注册后不得改动 schema 或替换回调;要热换工具就处置其所属 effect 再注册新的。
想做权限或埋点,别写进工具本体。 官方建议用 tools/pre-execute(放行 / 拒绝 / 追问策略)、ctx.tools.guard()(不可撤销的最终拒绝)、tools/execute(环绕派发加超时、重试、指标)、tools/post-execute(替换展示内容或结果)与 tools/result(观察不可变结果)这些扩展点,让工具体保持纯粹。
DSH plugin 的长时任务与 UI 卡片
耗时操作走后台任务通道,界面呈现交给纯函数卡片——两者都不要污染 canonical 值。 需要用 run_in_background 时,经 ctx.jobs.start({ kind, label, owner: exec.agent, run }) 注册,成功分支返回类型化的句柄(如 { kind: 'background', jobId }),Code Mode 绝不能去解析那句人类可读的 started background job bash-1 来取 id(来源)。
界面卡片通过两个可选方法声明,返回带 card 标签的渲染意图:
| 方法 | 卡片 | 适用场景 |
|---|---|---|
presentCall(args) | terminal | 你的调用本身就是一条 shell 命令 |
presentCall(args) | diff | 你的调用会创建或修改文件 |
presentCall(args) | generic | 默认卡片,可带 kind 图标与 locations 跳转 |
presentResult(args, result) | 同名卡片 | 完成态:终端输出、已应用的 diff、搜索结果等 |
两条会咬人的硬规则:① 这些函数在实时流与会话回放中都会执行,因此必须是 args(与结果)的纯函数——不许 I/O、不许读会话状态、不许看时钟或随机数;② 界面专用格式不要混进模型结果,output.render 管面向模型的文案,presentationMeta 加卡片呈现管界面状态。
工具没有任何界面呈现时,会回退到通用卡片(标题 = 工具名,输入 = 原始参数),不会崩。界面相关的更多呈现方式见 插件界面开发。
DSH plugin 工具插件的自检与下一步
注册前过一遍这五项:
inject里有没有tools;parameters与output.schema是否描述清楚,description是否面向模型;execute是否只返回 canonical 值、是否正确区分「抛异常」与「非理想结果」;- 是否兑现了
exec.signal; - 卡片呈现函数是否为纯函数。
需要可替换的实现时再拆包:把定义(Service Definition)、实现(Provider)、消费方(暴露成 tool)拆成三个包,详见 开发指南 的三角色设计;打包与发布见 打包成 bundle 与 发布到插件中心。插件装好后可在 DSH Plugin Hub 的已安装列表确认状态。
常见问题
**DSH plugin 注册一个模型可调用的工具只需三步:① export const inject = ['tools'] 让框架先备好工具注册表;② 在 apply 里 ctx.tools.register(defineTool({ ... }));③ 在 defineTool 里声明 parameters、output.schema 与 execute。** 注册是 effect 式的:插件 fiber 被处置时工具会自动注销(来源:官方「构建一个工具」)。
**在 DSH plugin 的 defineTool 里,parameters 管入参、output.schema 管出参,两者不要混用。** parameters 描述模型可以传什么参数,defineTool 会据此自动推导并校验 execute 的 args 类型;output.schema 描述 execute 必须返回的唯一 canonical 值,面向模型的文案交给 output.render 转换(来源:官方「工具编写参考」)。
**不能——DSH plugin 的 execute 只返回 output.schema 声明的那个 canonical JSON 值**,注册表会把它快照、校验、冻结后交给 output.render(args, value) 转成模型可见内容。官方明确要求不要在函数体里返回内容块、也不要让调用方去解析散文取 id 或字段(来源:官方「工具编写参考」)。
**DSH plugin 工具处理错误有两条规则:基础设施故障直接 throw(注册表会捕获并标记 isError),业务上的非理想结果仍返回 canonical 值**,交给渲染器解释(例如进程非零退出)。另外必须遵守 exec.signal,它触发时取消尚未完成的工作(来源:官方「工具编写参考」)。
**DSH plugin 用 presentCall(args) 与 presentResult(args, result) 返回带 card 标签的渲染意图来定制卡片**:终端命令用 terminal、文件改动用 diff、其余用 generic(可带 kind 图标与 locations 跳转)。这两个函数会在实时流与回放中执行,必须是纯函数——不读文件、不读会话状态、不看时钟,否则回放会崩(来源:官方「工具编写参考」)。
相关术语
- defineTool
- defineTool 是 DSH plugin 声明模型可调用工具的辅助函数,接收 name、description、parameters、output、execute 等字段,返回可直接交给 ctx.tools.register 的工具定义,并会从 parameters 推导 execute 的入参类型。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
- canonical value(规范值)
- canonical value 是 DSH plugin 工具 execute 按 output.schema 返回的唯一结构化结果,注册表会把它快照为无损 JSON、校验并冻结,再交给 output.render 生成面向模型的内容。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
- exec.signal
- exec.signal 是 DSH plugin 工具执行上下文里的取消信号,必须在触发时终止进行中的工作;它是被保护的执行身份之一,调用方与包装器都不得移除。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
- presentCall / presentResult
- presentCall 与 presentResult 是 DSH plugin 工具的可选界面投影方法,分别返回调用中与完成后的卡片渲染意图(generic / terminal / diff / search / web),供宿主渲染;它们必须是对 args 与结果的纯函数。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
来源
- DeepSeek Harness 官方文档 - 构建一个工具· deepseek-ai
- DeepSeek Harness 官方文档 - 工具编写参考(execute 契约与卡片)· deepseek-ai
- DeepSeek Harness 官方文档 - 三角色能力设计· deepseek-ai