DeepSeek Harness 系列(02):万物皆插件——Cordis 核心设计深度解读
英文标题:DeepSeek Harness 系列(02):万物皆插件——Cordis 核心设计深度解读
dsh 的所有能力都建立在 Cordis 插件框架之上。这篇文章结合真实源码,深度解读 Cordis 五个核心机制:Plugin、Context、Service、Event、Effect
6.3 KB
中文
Cordis 是什么
在开始讲机制之前,先搞清楚这个问题:Cordis 是什么东西,它从哪里来,为什么 dsh 要用它?
它不是 DeepSeek 自己写的
Cordis 是一个独立的开源 TypeScript 插件框架 ,不是 DeepSeek 写的,来自 cordiverse 社区。DeepSeek 把它 vendor(内嵌)进了 dsh 的代码仓库(放在 vendor/cordis/),并做了一些定制化修改。
它的定位,用官方 README 的话说:
a TypeScript plugin framework for applications that need explicit dependency injection, scoped services, lifecycle-managed cleanup, and optional configuration-driven loading.
翻译成人话:一个帮你把应用拆成插件的框架,每个插件按需加载,卸载时自动清理,插件之间通过「服务」和「事件」通信而不是相互 import。
插件框架解决什么问题
假设你要从零搭一个 Agent 系统。最朴素的写法大概是这样:
import { ToolsRegistry } from './tools'
import { SessionStore } from './session'
import { AgentLoop } from './agent-loop'
import { LLMAdapter } from './llm-deepseek'
const tools = new ToolsRegistry()
const session = new SessionStore()
const llm = new LLMAdapter()
const loop = new AgentLoop(tools, session, llm)
loop.run()
这样写能跑,但有几个问题:
- 换个组件很麻烦 :想把
LLMAdapter换成另一家的,要找到所有import的地方改 - 加载顺序靠手工保证 :
AgentLoop必须在其他三个都初始化之后才能创建 - 没有热重载 :改一个配置只能重启整个进程
- 清理是噩梦 :
session要关连接,loop要停定时器,谁来保证顺序?
Cordis 的解法 :不直接 import,通过一个共享的 ctx 容器互相发现;加载顺序由依赖声明自动推导;每个注册都有配套的清理函数,卸载时自动执行。
这就是为什么 dsh 选了它:dsh 要支持热重载、任意替换模型/沙箱/工具提供方,Cordis 天生就是为这个场景设计的。
最小示例:五行理解核心思路
import { Context, Service } from 'cordis'
const root = new Context()
await root.plugin(ToolsService)
await root.plugin(AgentLoop)
await root.plugin(ToolsService).dispose()
一个 ctx,所有东西挂在上面,互相通过 ctx 发现对方,卸载时自动反向清理——这就是 Cordis 的核心思路。dsh 里有几十个插件,全都用这个方式组织。
为什么要先学 Cordis
dsh 没有传统意义上的「框架核心」。模型适配器是插件,工具系统是插件,Agent 循环本身也是插件——甚至 Session 日志、权限控制、沙箱隔离,都是以插件形式挂载的。
这不是设计夸张,是字面意思。官方文档直接说:
There is no privileged core to patch: you extend dsh by mounting a plugin beside the others.
驱动这一切的底层框架就是 Cordis 。理解了 Cordis,就理解了 dsh 的所有扩展机制。本文结合 Cordis 源码(位于 vendor/cordis/src/),逐一讲透它的五个核心概念。
一、Context:不是对象,是代理
我们先从最基础的开始。每个 Cordis 插件都会收到一个 ctx 参数,但这个 ctx 不是普通的对象——它是一个 Proxy(代理) 。
源码(vendor/cordis/src/context.ts):
constructor() {
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
}
为什么用 Proxy?因为 dsh 要实现服务按需查找 :当你写 ctx.tools 时,不是从一个普通对象上读一个属性——而是触发代理,去服务注册表里找名为 'tools' 的服务实例。
这个设计解决了一个核心问题:消费方不需要 import 提供方 。
import { ToolsService } from './tools-service'
const tools = new ToolsService()
ctx.tools.register(myTool)
当你换掉工具服务的实现(比如切换到远程工具执行),消费方代码一行都不用改。
Context 是有层级的
每个插件运行在自己的 子 Context 里,子 Context 继承父 Context 的服务,但可以有自己的隔离服务实例。这是 dsh 实现「每个 Agent 有自己的工具集」的底层机制。
二、Plugin + Fiber:插件的生命周期
插件的三种写法
Cordis 接受三种形式的插件:
import { Service, type Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
console.log('插件已加载')
}
export const myPlugin = {
name: 'my-plugin',
apply(ctx: Context) { },
}
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
Fiber:插件实例的状态机
每个被加载的插件实例都有一个对应的 Fiber 。Fiber 是 Cordis 追踪插件生命周期的核心对象。
源码(vendor/cordis/src/fiber.ts)里定义了六个状态:
export const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
UNLOADING,
DISPOSED,
}
状态转换图:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
PENDING 状态是关键 :当一个插件声明了 inject: ['tools'],而 tools 服务还没有被加载,这个插件就会停在 PENDING,什么都不做,既不报错也不启动。等 tools 服务就绪,它才会自动进入 LOADING。
这个设计的好处:插件不需要关心加载顺序。你可以在 cordis.yml 里按任何顺序列插件,Cordis 会根据依赖关系自动排序。
三、Effect:可逆的注册
这是 Cordis 最重要也最容易被忽视的设计。
问题:谁来清理?
传统的事件监听或资源注册,如果你忘了手动清理,会造成内存泄漏:
emitter.on('data', handler)
Cordis 的解法是:每个注册都是一个 Effect,有对应的 disposer(清理函数) ,插件卸载时自动执行所有 disposer。
ctx.effect():包装可逆操作
源码(vendor/cordis/src/fiber.ts)的核心逻辑:disposer 在插件卸载时按注册顺序的逆序 执行。
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('心跳'), 1000)
return () => {
clearInterval(timer)
console.log('定时器已清理')
}
})
}
内置的 Effect
好消息是,大多数情况下你不需要亲自写 ctx.effect(),因为 Cordis 的内置 API 已经自带 Effect:
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => { })
ctx.plugin(ChildPlugin)
ctx.tools.register(myTool)
ctx.effect(() => {
const conn = createDatabaseConnection()
return () => conn.close()
})
}
为什么这个设计很重要
这是 dsh 实现**热重载(Hot Reload)**的基础。当你修改一个插件的配置,Cordis 会:
- 卸载旧版本(所有 Effect 的 disposer 自动执行)
- 用新配置加载新版本
整个过程不需要重启进程,也不会有任何遗留的监听器或未关闭的连接。
四、Service:依赖倒置的工程实现
提供服务
创建一个服务,需要继承 Service 类,在构造函数里传入服务名:
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}!`
}
}
源码(vendor/cordis/src/service.ts)里 Service 构造函数的核心逻辑:
constructor(protected ctx: Context, name: string) {
self.ctx.reflect.provide(name, self, this[symbols.check])
return self
}
消费服务
消费方只需要声明 inject,不需要知道是哪个包提供的服务:
export const name = 'my-plugin'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('dsh'))
}
动态追踪:不只是启动时检查
inject 不是一次性的启动检查。如果应用运行期间,greeter 服务被卸载(比如热重载了提供方插件),所有依赖它的插件会自动卸载 ,等服务恢复后自动重新加载 。
这是 dsh 能做到「换一行配置,整个能力体系跟着换」的原因。比如把默认的本地 Shell 换成远程沙箱:
- id: shell-provider
name: '@deepseek-ai/dsh-sandbox-remote'
五、Event:五种分发模式,各有用途
Cordis 的事件系统不只有一种 emit,而是五种分发模式,对应不同的业务场景。
声明事件类型
先用 TypeScript 声明合并注册事件类型(interface Events):
declare module '@deepseek-ai/cordis' {
interface Events {
'tool/executed'(name: string, duration: number): void
}
}
这纯粹是 TypeScript 编译时的类型声明,不生成任何运行时代码,但让 ctx.emit、ctx.on 全部有正确的类型提示。
五种分发模式
看源码(vendor/cordis/src/events.ts)里的实现,每种模式的行为一目了然:
ctx.emit('tool/executed', 'search', 120)
await ctx.parallel('session/sync', sessionId)
const handler = await ctx.serial('approval/request', toolCall)
const result = ctx.bail('format/render', content)
Waterfall:dsh 的核心拦截机制
Waterfall 是理解 dsh 如何让插件"拦截和改写"行为的关键。
它的工作方式类似洋葱模型(Koa 的 middleware):每个监听器包裹着后面所有监听器,可以选择传递(调用 next())或短路(不调用 next())。
declare module '@deepseek-ai/cordis' {
interface Events {
'agent/pre-step'(messages: Message[], next: () => Promise<Decision>): Promise<Decision>
}
}
ctx.on('agent/pre-step', async (messages, next) => {
if (containsSensitiveContent(messages)) {
return { type: 'reject', reason: '包含敏感内容' }
}
return next()
})
ctx.on('agent/pre-step', async (messages, next) => {
console.log(`[pre-step] 处理 ${messages.length} 条消息`)
const result = await next()
console.log(`[pre-step] 结果: ${result.type}`)
return result
})
一条铁律 :只做观察/记录的 waterfall 监听器必须 调用 next()。不调用就代表"我拥有这个决策权,后面的都不用看了"。忘记调用 next() 会静默截断所有下游逻辑,这是 Cordis 文档里强调的一个常见错误。
dsh 中大量使用 waterfall 来实现可插拔的策略:
agent/pre-step:决定是否接受用户输入agent/request:可以替换模型调用配置(比如路由到不同模型)tools/pre-execute:工具执行前的权限检查approval/request:权限审批策略
六、Profile + Bundle:配置组合成产品
理解了上面的机制,再看 dsh 的 Profile/Bundle 系统就很直观了。
Bundle 就是一份插件配置列表(YAML 文件),描述「挂载哪些插件、用什么配置」:
- insert:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
- id: tools
name: '@deepseek-ai/dsh-tools'
- id: session-persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
Profile 是按顺序叠加的 bundle 列表,后面的 bundle 可以覆盖前面的配置:
web profile = dsh-base + dsh-web-app + 用户的 cordis.patch.yml
↑ 用户可以在这里覆盖任何行
用 --dump-config 看一眼实际加载的插件树:
dsh --profile web --dump-config
七、完整示例:写一个 dsh 插件
把上面所有概念串在一起,写一个完整的工具插件:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool-plugin'
export const inject = ['tools', 'systemPrompt']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'get_time',
description: '获取当前时间',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute() {
return new Date().toLocaleString('zh-CN')
},
}))
ctx.on('tools/result', (exec, result) => {
if (exec.name === 'get_time') {
console.log(`[日志] get_time 被调用,返回: ${JSON.stringify(result.content)}`)
}
})
}
通过 cordis.patch.yml 把这个插件加入 dsh:
- insert:
- id: my-tool
name: './my-tool-plugin.ts'
这就是完整的 dsh 插件开发流程。不需要 fork 框架,不需要修改任何内部代码,挂一个插件,能力就进来了。卸载插件,一切恢复原状。
设计哲学小结
回顾 Cordis 的五个核心机制,每一个都在解决一个具体问题:
| 机制 | 解决的问题 |
|---|---|
| Context(代理) | 消费方不依赖具体实现,可随时换掉提供方 |
| Fiber(状态机) | 插件按依赖顺序加载,PENDING 代替启动报错 |
| Effect(可逆注册) | 热重载和清理不需要手动管理,杜绝资源泄漏 |
| Service(依赖注入) | 通过 inject 声明依赖,运行时自动追踪,提供方可动态替换 |
| Event(五种模式) | 观察/拦截/策略决策/并发协调,每种场景用对应模式 |
这五个机制组合在一起,让 dsh 做到了:任何部分都可以被替换,但系统整体保持稳定运行 。这不是偶然,是系统性设计的结果。
下一篇预告 :系列第三篇——工具系统 。我们会深入 ctx.tools 的完整实现:注册机制、schema 自动生成、三阶段执行 pipeline,以及权限审批是怎么拦截危险操作的。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页
English / 原文
Cordis 是什么
在开始讲机制之前,先搞清楚这个问题:Cordis 是什么东西,它从哪里来,为什么 dsh 要用它?
它不是 DeepSeek 自己写的
Cordis 是一个独立的开源 TypeScript 插件框架 ,不是 DeepSeek 写的,来自 cordiverse 社区。DeepSeek 把它 vendor(内嵌)进了 dsh 的代码仓库(放在 vendor/cordis/),并做了一些定制化修改。
它的定位,用官方 README 的话说:
a TypeScript plugin framework for applications that need explicit dependency injection, scoped services, lifecycle-managed cleanup, and optional configuration-driven loading.
翻译成人话:一个帮你把应用拆成插件的框架,每个插件按需加载,卸载时自动清理,插件之间通过「服务」和「事件」通信而不是相互 import。
插件框架解决什么问题
假设你要从零搭一个 Agent 系统。最朴素的写法大概是这样:
import { ToolsRegistry } from './tools'
import { SessionStore } from './session'
import { AgentLoop } from './agent-loop'
import { LLMAdapter } from './llm-deepseek'
const tools = new ToolsRegistry()
const session = new SessionStore()
const llm = new LLMAdapter()
const loop = new AgentLoop(tools, session, llm)
loop.run()
这样写能跑,但有几个问题:
- 换个组件很麻烦 :想把
LLMAdapter换成另一家的,要找到所有import的地方改 - 加载顺序靠手工保证 :
AgentLoop必须在其他三个都初始化之后才能创建 - 没有热重载 :改一个配置只能重启整个进程
- 清理是噩梦 :
session要关连接,loop要停定时器,谁来保证顺序?
Cordis 的解法 :不直接 import,通过一个共享的 ctx 容器互相发现;加载顺序由依赖声明自动推导;每个注册都有配套的清理函数,卸载时自动执行。
这就是为什么 dsh 选了它:dsh 要支持热重载、任意替换模型/沙箱/工具提供方,Cordis 天生就是为这个场景设计的。
最小示例:五行理解核心思路
import { Context, Service } from 'cordis'
const root = new Context()
await root.plugin(ToolsService)
await root.plugin(AgentLoop)
await root.plugin(ToolsService).dispose()
一个 ctx,所有东西挂在上面,互相通过 ctx 发现对方,卸载时自动反向清理——这就是 Cordis 的核心思路。dsh 里有几十个插件,全都用这个方式组织。
为什么要先学 Cordis
dsh 没有传统意义上的「框架核心」。模型适配器是插件,工具系统是插件,Agent 循环本身也是插件——甚至 Session 日志、权限控制、沙箱隔离,都是以插件形式挂载的。
这不是设计夸张,是字面意思。官方文档直接说:
There is no privileged core to patch: you extend dsh by mounting a plugin beside the others.
驱动这一切的底层框架就是 Cordis 。理解了 Cordis,就理解了 dsh 的所有扩展机制。本文结合 Cordis 源码(位于 vendor/cordis/src/),逐一讲透它的五个核心概念。
一、Context:不是对象,是代理
我们先从最基础的开始。每个 Cordis 插件都会收到一个 ctx 参数,但这个 ctx 不是普通的对象——它是一个 Proxy(代理) 。
源码(vendor/cordis/src/context.ts):
constructor() {
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
}
为什么用 Proxy?因为 dsh 要实现服务按需查找 :当你写 ctx.tools 时,不是从一个普通对象上读一个属性——而是触发代理,去服务注册表里找名为 'tools' 的服务实例。
这个设计解决了一个核心问题:消费方不需要 import 提供方 。
import { ToolsService } from './tools-service'
const tools = new ToolsService()
ctx.tools.register(myTool)
当你换掉工具服务的实现(比如切换到远程工具执行),消费方代码一行都不用改。
Context 是有层级的
每个插件运行在自己的 子 Context 里,子 Context 继承父 Context 的服务,但可以有自己的隔离服务实例。这是 dsh 实现「每个 Agent 有自己的工具集」的底层机制。
二、Plugin + Fiber:插件的生命周期
插件的三种写法
Cordis 接受三种形式的插件:
import { Service, type Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
console.log('插件已加载')
}
export const myPlugin = {
name: 'my-plugin',
apply(ctx: Context) { },
}
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
Fiber:插件实例的状态机
每个被加载的插件实例都有一个对应的 Fiber 。Fiber 是 Cordis 追踪插件生命周期的核心对象。
源码(vendor/cordis/src/fiber.ts)里定义了六个状态:
export const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
UNLOADING,
DISPOSED,
}
状态转换图:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
PENDING 状态是关键 :当一个插件声明了 inject: ['tools'],而 tools 服务还没有被加载,这个插件就会停在 PENDING,什么都不做,既不报错也不启动。等 tools 服务就绪,它才会自动进入 LOADING。
这个设计的好处:插件不需要关心加载顺序。你可以在 cordis.yml 里按任何顺序列插件,Cordis 会根据依赖关系自动排序。
三、Effect:可逆的注册
这是 Cordis 最重要也最容易被忽视的设计。
问题:谁来清理?
传统的事件监听或资源注册,如果你忘了手动清理,会造成内存泄漏:
emitter.on('data', handler)
Cordis 的解法是:每个注册都是一个 Effect,有对应的 disposer(清理函数) ,插件卸载时自动执行所有 disposer。
ctx.effect():包装可逆操作
源码(vendor/cordis/src/fiber.ts)的核心逻辑:disposer 在插件卸载时按注册顺序的逆序 执行。
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('心跳'), 1000)
return () => {
clearInterval(timer)
console.log('定时器已清理')
}
})
}
内置的 Effect
好消息是,大多数情况下你不需要亲自写 ctx.effect(),因为 Cordis 的内置 API 已经自带 Effect:
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => { })
ctx.plugin(ChildPlugin)
ctx.tools.register(myTool)
ctx.effect(() => {
const conn = createDatabaseConnection()
return () => conn.close()
})
}
为什么这个设计很重要
这是 dsh 实现**热重载(Hot Reload)**的基础。当你修改一个插件的配置,Cordis 会:
- 卸载旧版本(所有 Effect 的 disposer 自动执行)
- 用新配置加载新版本
整个过程不需要重启进程,也不会有任何遗留的监听器或未关闭的连接。
四、Service:依赖倒置的工程实现
提供服务
创建一个服务,需要继承 Service 类,在构造函数里传入服务名:
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}!`
}
}
源码(vendor/cordis/src/service.ts)里 Service 构造函数的核心逻辑:
constructor(protected ctx: Context, name: string) {
self.ctx.reflect.provide(name, self, this[symbols.check])
return self
}
消费服务
消费方只需要声明 inject,不需要知道是哪个包提供的服务:
export const name = 'my-plugin'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('dsh'))
}
动态追踪:不只是启动时检查
inject 不是一次性的启动检查。如果应用运行期间,greeter 服务被卸载(比如热重载了提供方插件),所有依赖它的插件会自动卸载 ,等服务恢复后自动重新加载 。
这是 dsh 能做到「换一行配置,整个能力体系跟着换」的原因。比如把默认的本地 Shell 换成远程沙箱:
- id: shell-provider
name: '@deepseek-ai/dsh-sandbox-remote'
五、Event:五种分发模式,各有用途
Cordis 的事件系统不只有一种 emit,而是五种分发模式,对应不同的业务场景。
声明事件类型
先用 TypeScript 声明合并注册事件类型(interface Events):
declare module '@deepseek-ai/cordis' {
interface Events {
'tool/executed'(name: string, duration: number): void
}
}
这纯粹是 TypeScript 编译时的类型声明,不生成任何运行时代码,但让 ctx.emit、ctx.on 全部有正确的类型提示。
五种分发模式
看源码(vendor/cordis/src/events.ts)里的实现,每种模式的行为一目了然:
ctx.emit('tool/executed', 'search', 120)
await ctx.parallel('session/sync', sessionId)
const handler = await ctx.serial('approval/request', toolCall)
const result = ctx.bail('format/render', content)
Waterfall:dsh 的核心拦截机制
Waterfall 是理解 dsh 如何让插件"拦截和改写"行为的关键。
它的工作方式类似洋葱模型(Koa 的 middleware):每个监听器包裹着后面所有监听器,可以选择传递(调用 next())或短路(不调用 next())。
declare module '@deepseek-ai/cordis' {
interface Events {
'agent/pre-step'(messages: Message[], next: () => Promise<Decision>): Promise<Decision>
}
}
ctx.on('agent/pre-step', async (messages, next) => {
if (containsSensitiveContent(messages)) {
return { type: 'reject', reason: '包含敏感内容' }
}
return next()
})
ctx.on('agent/pre-step', async (messages, next) => {
console.log(`[pre-step] 处理 ${messages.length} 条消息`)
const result = await next()
console.log(`[pre-step] 结果: ${result.type}`)
return result
})
一条铁律 :只做观察/记录的 waterfall 监听器必须 调用 next()。不调用就代表"我拥有这个决策权,后面的都不用看了"。忘记调用 next() 会静默截断所有下游逻辑,这是 Cordis 文档里强调的一个常见错误。
dsh 中大量使用 waterfall 来实现可插拔的策略:
agent/pre-step:决定是否接受用户输入agent/request:可以替换模型调用配置(比如路由到不同模型)tools/pre-execute:工具执行前的权限检查approval/request:权限审批策略
六、Profile + Bundle:配置组合成产品
理解了上面的机制,再看 dsh 的 Profile/Bundle 系统就很直观了。
Bundle 就是一份插件配置列表(YAML 文件),描述「挂载哪些插件、用什么配置」:
- insert:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
- id: tools
name: '@deepseek-ai/dsh-tools'
- id: session-persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
Profile 是按顺序叠加的 bundle 列表,后面的 bundle 可以覆盖前面的配置:
web profile = dsh-base + dsh-web-app + 用户的 cordis.patch.yml
↑ 用户可以在这里覆盖任何行
用 --dump-config 看一眼实际加载的插件树:
dsh --profile web --dump-config
七、完整示例:写一个 dsh 插件
把上面所有概念串在一起,写一个完整的工具插件:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool-plugin'
export const inject = ['tools', 'systemPrompt']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'get_time',
description: '获取当前时间',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute() {
return new Date().toLocaleString('zh-CN')
},
}))
ctx.on('tools/result', (exec, result) => {
if (exec.name === 'get_time') {
console.log(`[日志] get_time 被调用,返回: ${JSON.stringify(result.content)}`)
}
})
}
通过 cordis.patch.yml 把这个插件加入 dsh:
- insert:
- id: my-tool
name: './my-tool-plugin.ts'
这就是完整的 dsh 插件开发流程。不需要 fork 框架,不需要修改任何内部代码,挂一个插件,能力就进来了。卸载插件,一切恢复原状。
设计哲学小结
回顾 Cordis 的五个核心机制,每一个都在解决一个具体问题:
| 机制 | 解决的问题 |
|---|---|
| Context(代理) | 消费方不依赖具体实现,可随时换掉提供方 |
| Fiber(状态机) | 插件按依赖顺序加载,PENDING 代替启动报错 |
| Effect(可逆注册) | 热重载和清理不需要手动管理,杜绝资源泄漏 |
| Service(依赖注入) | 通过 inject 声明依赖,运行时自动追踪,提供方可动态替换 |
| Event(五种模式) | 观察/拦截/策略决策/并发协调,每种场景用对应模式 |
这五个机制组合在一起,让 dsh 做到了:任何部分都可以被替换,但系统整体保持稳定运行 。这不是偶然,是系统性设计的结果。
下一篇预告 :系列第三篇——工具系统 。我们会深入 ctx.tools 的完整实现:注册机制、schema 自动生成、三阶段执行 pipeline,以及权限审批是怎么拦截危险操作的。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页