Skip to content
源码版本47f9438 (dsh@0.1)

Cordis Context

Context 是 Cordis 的根容器。它不是一个普通对象,而是一个被 ReflectService.handler 代理的 Proxy——读普通属性会走 service resolver,而 extend()/isolate()/intercept() 用原型链创建子 context,不修改父级。Context 既承载内置服务,又通过 TypeScript 的 declare module 接口合并让第三方插件声明新的 ctx.<key>

Context 的公共形状

vendor/cordis/src/context.ts:16-33 定义了 interface Context:
  • 四个内置服务:events/logger/reflect/registry
  • rootbaseUrl 指针
  • [symbols.isolate][symbols.intercept] 两个 symbol 内部映射

这些字段不是普通属性,而是通过 Context 的 Proxy 转发到 service store。

Context 是一个 Proxy

最直接的物证在 vendor/cordis/src/context.ts:42-84:class Context 的构造器里:

ts
new Proxy<this>(this, ReflectService.handler)

构造器创建 root context 并挂载内置服务,root fiber 用空 runtime。这就是"Context 是 proxy"的根因——所有属性读写都被 ReflectService 拦截。

跨副本识别

vendor/cordis/src/context.ts:61-68 实现了跨 realm/跨 cordis 副本识别:
ts
static is(value) { ... }
static { Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is') }

它用全局 symbol 而非 instanceof 来识别 context。这一点很关键:dsh 大量使用 vm 沙箱(动态插件),沙箱里的 instanceof 在 proxy 上失效,所以 Cordis 用全局 symbol 绕过。

分层:extend 与原型链

vendor/cordis/src/context.ts:99-107extend(meta):
ts
extend(meta) {
  return Object.create(getTraceable(this, this))  // 原型链子 context
}

extend 返回 Object.create(this) 的对象,meta 的 own key 覆盖继承值,父 context 不被修改。这意味着 Context 的"分层"靠 JS 原型链,不是不可变值——读走原型、写只写自己。

隔离:isolate

vendor/cordis/src/context.ts:121-125isolate(name, label?):
ts
isolate(name, label?) {
  // 复制 isolate map 并给 name 一个新 symbol scope
}

同一个 service 名(如 shell)在两个 isolate('shell') 子树里可以注册不同实现,互不影响。这对 multi-agent / multi-session 场景至关重要——每个会话可以有自己的 shell 实例。

拦截:intercept

vendor/cordis/src/context.ts:139-145intercept(name, config):
ts
intercept(name, config) {
  // 把 intercept config 挂到子 context
}

Service 启动时由 [symbols.resolveConfig] 沿原型链合并。这是 Cordis DI 的"参数注入"等价物——ctx.intercept('llm', { provider: 'x' }) 让子树所有插件看到的 llm config 合并了这段 override,但根 context 不受影响。

Service store 与 mixin

官方文档 docs/cordis-api/context.md:236-339 列出了 ctx.get/ctx.set/ctx.provide/ctx.accessor/ctx.mixin 这些"Service store and mixins" API。它们的实际定义在 reflect.ts,但通过 Context proxy 暴露:

  • 写一个 ctx.foo 会触发 ReflectService handler 把值放进 service store
  • ctx.foo 则从 store 解析当前 scope 内的 impl
  • ctx.onctx.events.on 这种 mixin 转发的 accessor,让 service 实例可替换而 ctx.on 始终指向当前 active impl

为什么用 symbol 而非字符串

Context 的 Context.effect/Context.filter/Context.isolate/Context.intercept 都是 unique symbol 静态字段。插件作者用 ctx[Context.effect] 等访问元数据,避免与字符串 key 冲突——因为 ctx 是 proxy,任何字符串属性访问都会被 service resolver 拦截,只有 symbol 能直达内部映射。

接下来读什么