实现一个叫 myCap 的能力:输入一段文本,输出全部大写。三步走:
Service Definition(抽象类 + 类型)→ Service Provider(实现子类)→ Consumer(defineTool),最后在
cordis.yml里把 Provider 与 Consumer 组合加载。
第一步:编写 Service Definition
声明能力本身:服务叫什么、怎么调用、请求与结果的类型。不含实现逻辑,只有一个抽象方法和两个接口。
1// 文件路径:packages/my-cap/my-cap/src/index.ts
2import { Service, type Context } from '@deepseek-ai/cordis'
3
4// 声明合并:让 ctx.myCap 在 TypeScript 里有类型提示
5declare module '@deepseek-ai/cordis' {
6 interface Context {
7 myCap: MyCapService
8 }
9}
10
11// 抽象类:Definition 包只声明契约,不写实现
12export abstract class MyCapService extends Service {
13 constructor(ctx: Context) {
14 super(ctx, 'myCap') // 注册为命名服务 ctx.myCap
15 }
16
17 /** Execute the capability. */
18 abstract execute(request: MyCapRequest): Promise<MyCapResult>
19}
20
21// 请求类型:调用方必须提供 input
22export interface MyCapRequest {
23 input: string
24}
25
26// 结果类型:能力返回 output
27export interface MyCapResult {
28 output: string
29}
abstract execute是唯一的抽象方法,Provider 必须实现它。- 请求与结果类型由 Definition 包拥有,Provider 与 Consumer 都从它导入。
第二步:编写 Service Provider
Provider 继承抽象类,填上真正的行为。
1// 文件路径:packages/my-cap/my-cap-local/src/index.ts
2import type { Context } from '@deepseek-ai/cordis'
3import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
4
5// 实现类:只依赖 Definition 包
6class MyCapLocal extends MyCapService {
7 async execute(request: MyCapRequest): Promise<MyCapResult> {
8 // Local provider behavior.
9 return { output: request.input.toUpperCase() }
10 }
11}
12
13export const name = 'my-cap-local'
14
15export function apply(ctx: Context) {
16 // 把实现类作为插件加载,注册成 ctx.myCap 的实际服务
17 ctx.plugin(MyCapLocal)
18}
Provider 只依赖 Definition 包里的抽象类和类型,不关心模型怎么调用、工具长什么样。将来想换实现(如远程服务器),只需再写一个 Provider 子类。
第三步:编写消费方 Consumer
Consumer 把能力包装成模型可调用的工具。
1// 文件路径:packages/my-cap/tool-my-cap/src/index.ts
2import type { Context } from '@deepseek-ai/cordis'
3import { defineTool } from '@deepseek-ai/dsh-tools'
4
5export const name = 'tool-my-cap'
6// 依赖声明:tools 提供注册入口,myCap 提供服务实现
7export const inject = ['tools', 'myCap']
8
9export function apply(ctx: Context) {
10 ctx.tools.register(defineTool({
11 name: 'my_cap', // 面向模型的工具名
12 description: 'Execute my capability.',
13 parameters: {
14 input: { type: 'string', required: true },
15 },
16 output: {
17 schema: { type: 'string' },
18 render: (_args, value) => [{ type: 'text', text: value }],
19 },
20 async execute(args) {
21 const result = await ctx.myCap.execute({ input: args.input })
22 return result.output
23 },
24 }))
25}
模型不直接认识 ctx.myCap,它只认识 my_cap 工具。Consumer 是两者之间的桥梁,也负责把结果渲染成模型可见的文本。
数据流
模型 → my_cap 工具(Consumer 层)→ Consumer 包装参数为 MyCapRequest 交给 ctx.myCap.execute(request) → Definition 委托给当前加载的 Provider → Provider 返回 MyCapResult → Consumer 把 output 渲染成文本作为工具结果交给模型。整条链路里 Provider 被换掉时,Consumer 与模型都无感知。
在 cordis.yml 中组合
1# 文件路径:cordis.yml
2# 先加载 Provider,让 ctx.myCap 有实现
3- name: '@deepseek-ai/dsh-my-cap-local'
4
5# 再加载 Consumer,让模型能用 my_cap 工具
6- name: '@deepseek-ai/dsh-tool-my-cap'
加载顺序上 Cordis 会按依赖自动排序,Provider 与 Consumer 谁先谁后不关键。想换 Provider 时只改第一行。
设计要点(三条)
- 不要预防性拆分:只有角色需要独立演进时才拆包。简单的工具插件不需要拆分,一个包承担多个角色完全合法。
- Service Definition 拥有 Request/Result 类型:Provider 和 Consumer 只依赖 Definition 包,因此请求与结果类型必须由 Definition 定义。
- 显式优于隐式:实现应通过显式的
resolve(request)步骤处理默认值,而不是在run()里隐藏默认值。Bash seam 就是例子:ShellExecRequest的 workdir、timeoutMs 可选,工具层先调用ctx.shell.resolve(request)得到全部字段必填的ShellExecSpec,再交给run()。
不要在
run() 内部悄悄补默认值;先 resolve,再执行,边界才清晰。