

开发一个工具
本教程会在 Web UI 中添加一个 greet 工具。请先完成第一个插件,并保留其中的 scratch-plugin 目录。
创建工具插件
将 scratch-plugin/src/my-plugin.ts 替换为:
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, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
inject 让 Cordis 等待工具注册表就绪。defineTool 根据 parameters 推导并校验 args;execute 返回 output.schema 声明的规范值,output.render 再将该值转换为面向模型的内容。
运行并调用工具
如果开发命令未在运行,请重新启动:
pnpm dsh web –patch ./scratch-plugin/cordis.yml
打开 http://127.0.0.1:3080,然后输入:Use the greet tool to greet Ada. 模型可以调用 greet,并收到 Hello, Ada! 这一工具结果。
一、这段代码在做什么
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, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
拆开看:
- import type { Context } —— 只导入类型,运行时被擦除,不给插件增加运行时依赖。
- name / inject / apply —— 这是 Cordis 函数插件的固定三个导出,框架靠它们识别并加载这个插件。
- ctx.tools.register(…) —— 往 tools 服务里注册一个工具,返回的“注销器”会被框架接管,插件卸载时自动注销。
二、Cordis (这行代码背后的框架约定)
Cordis 是 Harness 底层的插件框架(vendored,见 [cordis-primer.md](file:///d:/harness/3/deepseek-harness/docs/cordis-primer.md)),五个核心思想正好对应这段代码:
几个关键点落到这段代码:
-
加载时序与 inject:inject: ['tools'] 让插件在 tools 服务出现前一直处于 PENDING 状态,不会报错、也不执行 apply。一旦 @deepseek-ai/dsh-tools 提供者挂载,apply 才运行,此时 ctx.tools 保证可用。所以 cordis.yml 里的顺序无关紧要,依赖关系决定启动顺序。
-
注册即 effect:ctx.tools.register(…) 返回的 disposer 被自动挂到插件生命周期上。插件被卸载(热重载 / 配置改动 / 服务消失)时,工具会被自动注销。你不用手动 removeTool。
-
apply 里 ctx.tools 的类型来源:ctx.tools 能通过类型检查,是因为 @deepseek-ai/dsh-tools 用了 TypeScript 声明合并(declare module '@deepseek-ai/cordis')往 Context 接口上加了 tools 属性。注意这段代码没写 import type {} from '@deepseek-ai/dsh-tools',但它确实 import 了 defineTool(这个包本身就带了声明合并),所以 ctx.tools 已类型化。
三、defineTool (工具 DSL)
defineTool 来自 @deepseek-ai/dsh-tools,它把参数 spec 转成模型可见的 JSON Schema,并推断 args 类型。详细契约见 [adding-a-tool.md](file:///d:/harness/3/deepseek-harness/docs/cookbook/adding-a-tool.md) 和 [tool.md](file:///d:/harness/3/deepseek-harness/docs/user/develop/basic/tool.md)。
三个字段各管一件事:
-
parameters → 给模型看的入参 schema,同时决定 execute(args) 里 args 的类型。这里 name: { type: 'string', required: true } 让 args.name 被推断为 string,且 defineTool 会在 execute 运行前校验模型传入的 arguments(类型、必填等)。
-
output.schema → 声明工具的“规范返回值”(canonical value),是程序化的、可持久化的 JSON。这里声明为 string。
-
output.render → 把 value 转成给模型看的 Native 内容块(content blocks)。这里把字符串包成 [{ type: 'text', text: value }]。
-
execute(args) → 真正干活的地方,返回的就是 output.schema 声明的规范值。这里返回 Hello, ${args.name}!。
一个关键心智模型:execute 返回的是给程序/日志用的规范值,render 才是给模型看的文本,两者分开。这样 PTC 模式(await tools.greet(…))能拿到干净的返回值,而模型能拿到渲染后的文本。
四、profile (这个插件如何被装进一个应用)
“profile”是 Harness 的**应用组合(插件树)**概念,定义在 [profile.ts](file:///d:/harness/3/deepseek-harness/packages/boot/app-boot/src/profile.ts#L1-L23) 的模块注释里:
- 一个 profile 是 $DSH_HOME/profiles/<name> 下的一个目录,包含:
- package.json —— 声明 dsh.profile.bundles(一个有序的 bundle 列表)和 patchReload。
- cordis.patch.yml —— 用户自己的 patch 层,在所有 bundle 层之后应用。
- bundle 是一个 npm 包,其 manifest 里声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }。组合时按 bundles 顺序把每个 bundle 的 patch 层叠加到空列表上,再叠 profile 自己的 patch,最后叠 launcher 层(–patch 文件和 flag 派生的 patch)。
内置 profile 模板(见 [profile.ts](file:///d:/harness/3/deepseek-harness/packages/boot/app-boot/src/profile.ts#L104-L126)):web、headless、acp、sdk 等,每个由一组 bundle 组成。例如 headless = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless']。
- [base 层 cordis.patch.yml](file:///d:/harness/3/deepseek-harness/packages/bundle/base/cordis.patch.yml) 是共享核心,其中第 460-461 行就 mount 了 tools 服务:- id: tools → @deepseek-ai/dsh-tools。所以你的 inject: ['tools'] 能在这里被满足。
- [headless 层](file:///d:/harness/3/deepseek-harness/packages/bundle/headless/cordis.patch.yml) 在 base 之上又插入自己的 patch。
这个 greet-tool 插件怎么挂进去:通过 –patch overlay。教程里用的是 scratch-plugin/cordis.yml:
pnpm dsh web –patch ./scratch-plugin/cordis.yml
这个 overlay 会 insert 你的插件条目(路径要是绝对路径),叠加到 web profile 的插件树上,于是 greet 工具就出现在模型可调用的工具目录里。patch 文件只贡献配置,不改变 loader 解析模块路径的 profile 目录(见 [index.md](file:///d:/harness/3/deepseek-harness/docs/user/develop/basic/index.md))。
五、把它们串起来的一句话
defineTool 定义了工具契约 → ctx.tools.register 把它作为一个可逆 effect 注册进 tools 服务 → inject: ['tools'] 保证注册时服务已就绪 → profile 的 bundle/patch 层负责把 dsh-tools(服务提供者)和你的插件(服务消费者)组装进同一棵插件树,最终让模型能调用 greet。


