服务系统:provide 与 Service 基类
专栏:Cordis 插件框架 · 第 4 / 12 篇:::info 学习目标
完成本篇后你能够:解释 ctx.reflect.provide 与 ctx.provide 的关系;读懂 Service 基类构造器的每一步;理解拦截配置(intercept)的合并顺序。
前置:第 3 篇完成。预计时长:50 分钟。
:::
服务是 Cordis 里”能力”的载体。注册一条服务有两条路径:函数式 ctx.provide(name, value)(直接登记),或类式 class X extends Service(构造时自动登记)。两条路径最终都汇入 reflect 层。
Service 基类:构造器逐行读
// vendor/cordis/src/service.ts:42-59
constructor(protected ctx: Context, name: string) {
name ??= this.constructor['provide'] as string // 类可声明静态 provide 字段
let self = this
const tracker: Tracker = {
associate: name, // 诊断:这个实例关联哪个服务名
property: 'ctx',
}
if (self[symbols.invoke]) {
// 声明了 [Service.invoke] 的服务是"可调用"的:
// 把实例与 Function.prototype 拼成一个可 call 的代理
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker)
}
self.ctx = ctx
self.name = name
defineProperty(self, symbols.tracker, tracker)
// ★ 核心动作:登记进 reflect 层——服务随 owning fiber 卸载自动注销
self.ctx.reflect.provide(name, self, this[symbols.check])
return self
}
最后那行 provide 是服务”上线”的瞬间。从这一刻起,等待 'name' 服务的插件会被唤醒(第 5 篇的依赖门控);fiber 卸载时它自动消失。self[symbols.invoke] 分支解释了一个此前费解的现象——ctx.logger('name') 为什么能像函数一样调用 logger:LoggerService 声明了 invoke 符号,框架把”实例”与”函数”拼成同一个可调用对象。
作用域过滤:服务的 isolate 判定
// vendor/cordis/src/service.ts:61-63
protected [symbols.filter](ctx: Context) {
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name]
}
两行实现”这个服务对哪个上下文可见”:比较请求方与服务提供方的 isolate 标签——标签相同(同一作用域)才可见。dsh 的”每个 agent 一套工具”就是靠这个判定(第 10 篇)。
拦截配置的合并顺序
[symbols.resolveConfig](service.ts
// vendor/cordis/src/service.ts:86-102(节选)
[symbols.resolveConfig](base?: T, head?: T): T {
let intercept = this.ctx[Context.intercept]
const configs: any[] = []
while (this.name in intercept) { // 沿原型链向上收集
if (Object.hasOwn(intercept, this.name)) {
configs.unshift(intercept[this.name]) // 越靠近根的越先(低优先)
}
intercept = Object.getPrototypeOf(intercept)
}
if (base) configs.unshift(base)
if (head) configs.push(head)
...
return Object.assign({}, ...configs) // 后面的覆盖前面的
}
合并顺序:根侧的拦截最先,当前 ctx 的最后(最高优先),base 垫底、head 盖顶。服务声明了 Config.merge 时用它合并(处理需要深合并的场景),否则浅 assign。
一次 provide 的完整路径
与其他模块的联系
与其他模块的联系
- fiber(第 8 篇):
provide的第三参数check是可用性判定,注册本身登记为 fiber 的 effect——fiber 卸载即注销; - 依赖注入(第 5 篇):provide 会
notify等待该服务的全部 fiber,触发级联加载; - dsh:
ToolRuntime extends Service(构造器还顺手向 systemPrompt 注册工具提供方)、FileSystem extends Service(抽象基类等子类实现)、LlmRuntime extends Service——你在 dsh 里见到的每个ctx 键都长这样。
常见踩坑
- 同名服务二次 provide——被 reflect 层拒绝(重复提供服务),一个 ctx 一个实现;
- 在构造器里读
this.config——拦截配置的合并发生在resolveConfig,时机由框架控制,插件应通过注入的 config 参数拿配置; - 以为
ctx.x = y也能注册服务——Proxy 的 set 走internal/set校验,正规路径是 provide/Service。
随堂练习(带验收标准)
- 写一个
class Clock extends Service提供now()方法,并在另一插件注入使用。验收:注入处有完整类型(记得声明合并); - 给 Clock 加
[Service.invoke],使ctx.clock('now')可调用。验收:调用返回时间字符串; - 构造两层 ctx 各自 intercept 不同配置,打印
resolveConfig的合并结果,验证”根先子后、子覆盖根”。