函数插件的 apply 装完就完;服务插件把一个有名有姓的能力挂上 ctx 插座板供其他插件长期消费——tools、llm、agents 全是这么来的。判据:你写的东西会被别的插件 inject 吗?会,就写成服务。
import { Service, type Context } from '@deepseek-ai/cordis'
// ③ 类型登记:让 ctx.metrics 有类型提示(声明合并)
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
}
export default class MetricsService extends Service {
static inject = ['llm'] // ② 服务自己也可以依赖别的服务
constructor(ctx: Context) {
super(ctx, 'metrics') // ① 服务名 = 插座名
}
record(event: string, value: number) { // 公开方法 = 服务能力
/* ... */
}
}
消费方:
export const inject = ['metrics']
export function apply(ctx: Context) {
ctx.metrics.record('tool_call', 1) // 插座上有电了
}
export const inject = ['tools']
// 没有就不加载,卸载时自动销毁
const m = ctx.get('metrics')
m?.record('x', 1) // 有则用之
服务消失时:必需依赖它的插件自动销毁、回来自动重载,永不对空插座发电。可选必须用 ctx.get:属性代理 ctx.metrics 对拓扑敏感,严格读全局服务存储的 ctx.get 才是安全通道。
造一个"my-cap"能力,复刻 L06 的 Bash 三角色结构。
// packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context { myCap: MyCapService }
}
export abstract class MyCapService extends Service {
constructor(ctx: Context) { super(ctx, 'myCap') }
/** 执行该能力 */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
export interface MyCapRequest { input: string }
export interface MyCapResult { output: string }
要点:抽象类只定义形状不给实现;Request/Result 类型归合同包所有。
// packages/my-cap/my-cap-local/src/index.ts
import { MyCapService } from '@deepseek-ai/dsh-my-cap'
class MyCapLocal extends MyCapService {
async execute(request) { return { output: request.input.toUpperCase() } }
}
export const name = 'my-cap-local'
export function apply(ctx) { ctx.plugin(MyCapLocal) } // 挂上实现
// packages/my-cap/tool-my-cap/src/index.ts
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'tool-my-cap'
export const inject = ['tools', 'myCap'] // 同时依赖注册表和合同
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'my_cap',
description: 'Execute my capability.',
parameters: { input: { type: 'string', required: true } },
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
async execute(args) {
const result = await ctx.myCap.execute({ input: args.input })
return result.output
},
}))
}
# cordis.yml 组合(三行,换 Provider 换第一行)
- name: '@deepseek-ai/dsh-my-cap-local'
- name: '@deepseek-ai/dsh-tool-my-cap'
换个把 input 转小写的 Provider?只动第一行——L06 的承诺,现在亲手验证。
- id: group-a
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true # shell 服务在本组内单独一份
config:
- name: '@deepseek-ai/dsh-bash-local'
config: { timeoutMs: 5000 }
- name: './src/plugin-a.ts'
- id: group-b # 另一组,同样的服务,另一份实例、另一套配置
...
isolate 让同名服务在组内各有一份:两组各见各的 Bash 实例、各用各的超时,互不串门。
仓库要求每个注册贡献证明可销毁:销毁插件 fiber(fiber = 插件的运行实例),断言注册的东西真的消失(工具移除、监听器不再触发)。这把"注册即效果"变成 CI 硬约束;你的插件照此验收:HMR 干净 = 不变量成立。