服务与上下文:Cordis 如何共享能力而不绑定实现
从 Service、Context 与声明合并出发,理解提供者、消费者和作用域如何连接 DeepSeek Harness 的能力。
一句话:服务提供稳定的能力名称,Context 提供查找入口,消费者只依赖名称而不依赖 provider 的文件或类。
这让“换一个实现”不再意味着修改所有调用方:组合层换 provider,消费者仍然读取同一个 ctx.<key>。
01 / 一个服务有三个角色
一个可替换能力至少包含三类角色:
| 角色 | 负责什么 | Cordis 中的落点 |
|---|---|---|
| Service Definition | 规定能力名称和可调用接口 | TypeScript 类型与服务约定 |
| Service Provider | 创建实例并注册名称 | Service 子类或注册器 |
| Consumer | 使用能力完成自己的工作 | inject 加 ctx.service |
只有一方存在时,它还不是完整的可替换能力。Provider 没有 consumer,没人使用;consumer 直接导入 provider,则替换点又消失了。
02 / Provider 如何进入 Context
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 + '!'
}
}
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}super(ctx, 'greeter') 把实例注册到名为 greeter 的服务槽位。注册属于当前 Fiber 的 effect,provider 卸载时,服务也会从作用域中移除。
declare module 只做 TypeScript 声明合并,不产生运行时代码。运行时接线来自 Service 的注册;声明合并的价值是让消费者获得 ctx.greeter 的类型检查。
03 / Consumer 只声明需要什么
import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}inject 确保 apply 运行时服务已经存在。Consumer 不需要导入 GreeterService,因此配置可以把 greeter 替换为另一种 provider。
如果依赖是可选的,用 ctx.get('greeter') 探测。ctx.get() 默认只返回 ACTIVE 服务;没有可用 provider 时返回 undefined,也不会创建依赖边。不要在未声明注入时直接读取 ctx.greeter,属性代理可能抛出缺少 inject 的错误。
04 / Context 不止一个平面
Cordis 的 Context 是服务解析、插件注册和事件分发的入口。根 Context 之下可以创建子 Context:
extend()添加子作用域的元数据,但不修改父级。isolate(name)为某个服务创建独立作用域。intercept(name, config)为子树中的服务合并局部配置。
因此同一个服务名可以在不同组里指向不同实例。DeepSeek Harness 用这种思路支持 profile、agent preset 和隔离的能力组合。
05 / DSH 中的命名能力
在 Harness 架构中,工具注册表、LLM 适配器、Agent 注册表和会话存储都是通过 Context 连接的能力:
ctx.tools管理工具注册与执行流水线。ctx.llm承载消息、流和模型适配器能力。ctx.agents管理 live Agent 与agent/*事件。ctx.sessions管理持久的 session event log。
Provider 可以替换,Consumer 仍然使用稳定服务名。这就是 capability seam 能够把一个 provider 选择扩散成产品级行为的原因。
下一步
更多文章
动手:从 hello.ts 写出你的第一个 Cordis 插件
用一个函数插件和一份 cordis.yml,跑通 Loader、Context 与 Fiber 的最短路径。
Cordis:五个核心概念,读懂一棵插件树
用 Plugin、Context、inject、Events 与 Effect 五个概念,建立 Cordis 运行时的最小心智模型。
生命周期与 effect:让 Cordis 注册可以回卷
跟随 Fiber 从 PENDING 到 DISPOSED,理解 effect、disposer 与 HMR 如何共同管理插件副作用。
邮件列表
加入我们的社区
订阅邮件列表,及时获取最新消息和更新