上篇中,我们主要从Context类的定义介绍了它的extendisolateintercept方法,其中extend方法是核心,正是这个方法利用创建子Context的形式让Context具有无限的扩展可能。我们刻意漏掉了如下所示的两个部分:一是在extend方法中用来包装当前context对象的getTraceable函数,还有就是Context构造函数中将自己包装成代理的ReflectService.handler。其实这两者最终的目的只有一个:让绑定到固定Context上的同一个服务实例在执行的过程在能过自动跟踪当前提供的Context。换句话说,当我们将两个插件注册在不同的Context上并对同一个服务发起调用时,后者可以感知到作为调用者的时那个插件。

export class Context {     
    constructor() {
    ...
    const self = new Proxy<this>(this, ReflectService.handler)
    ...
    return self
  }

  extend(meta = {}): this {
    const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
    const self = Object.create(getTraceable(this, this))
    for (const prop of Reflect.ownKeys(meta)) {
      Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
    }
    if (!shadow) return self
    return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
  }
}

1. 让同一个服务在不同插件绑定的Context中执行

让注册服务具有跟踪(Trace)当前Context能力是通过如下这个getTraceable<T>函数赋予,它在整个跟踪体系中具有重要的作用。如果说有是否还有另一个函数比它更重要的化,那就是在它内部调用的createTraceable函数,因为具有跟踪能力的服务代理对象是由它创建出来的。如果直接介绍createTraceable的定义,我想很多人会晕头转向,所以我先通过简单的例子先说明这个方法通过封装指定服务对象,创建出的服务代理具有怎样的能力。由于createTraceable不能被导出,所以我们退而使用getTraceable<T>函数。

在如下的演示程序中,我们定义了一个模拟服务类型FoobarService。我们利用构造函数提供的参数ctx(同时定义成属性)提供绑定的Context,我们创建了Tranker对象作为symbols.tracker常量标识的属性,其propertyassociate和noShadow分别设置为ctxfoobarservice和同名参数值(默认为false)。

import { Context , getTraceable, symbols,Tracker} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
    interface Context   {
        foobar:string
    }
}
function checkShadowContext(ctx:Context, member:string)
{
    const shadow = (ctx as any)[symbols.shadow] as Context;
    const message = shadow
    ? `[${member}]ctx[symbols.shadow].foobar=${shadow.foobar}`
    : `[${member}]ctx[symbols.shadow]=undefined`;
    console.log(message);
}

class FoobarService{
    get foobar() { 
        checkShadowContext(this.ctx, "get foobar"); 
        return this.ctx.foobar
    }
    set foobar(value:string){ 
        checkShadowContext(this.ctx, "set foobar"); 
        this.ctx.foobar = value;
    }
    getFoobar(){checkShadowContext(
        this.ctx, "getFoobar"); 
        return this.ctx.foobar; 
    }
    constructor(public ctx: Context,noShadow:boolean = false){
        (this as any)[symbols.tracker] = {
            property:"ctx",  
            associate:"foobarservice",
            noShadow:noShadow};
    }
}

我们为FoobarService定义了三个成员,分别是用来读写this.ctx.foobar属性的gettersetter,和一个用来返回this.ctx.foobar的方法。为了验证Context是否提供了影子上下文,我们定义了checkShadowContext函数,并在这三个成员中执行。关于影子上下文,可以参阅我的文章DeepSeek Harness插件内核-05:如何让同一服务的方法在插件关联的上下文中执行?

1.1 noShadow = false

在如下的测试程序中,我们创建了两个作为根的rootContext,以及作为子ContextsubContext。以示区别,我们为它们定义了可读写的属性foobar,并将对应的值分别设置为foobar。作为服务实例的FoobarService对象最初是通过rootContext创建的。此时我们使用默认的noShadow参数,然后我们调用getTraceable针对subContext封装服务对象来生成代理。通过断言可以看出当我们调用只读属性foobar和方法getFoobar方法时,函数的执行上下文(this)发生了改变,导致this.ctx属性与当前的Context绑定在一起。表面看起来这是我们将Trackerproperty属性设置为 ctx 导致的结果。

const rootContext = new Context();
Object.defineProperty(rootContext, "foobar", {value: "foo", writable:true});

rootContext.provide("foobarservice.baz","baz");
const subContext = rootContext.extend();
Object.defineProperty(subContext, "foobar", {value: "bar", writable:true});

const service = new FoobarService(rootContext);
const serviceProxy = getTraceable(subContext, service);

console.assert(serviceProxy.foobar === "bar");
console.assert(serviceProxy.getFoobar() === "bar");
console.assert((serviceProxy as any).baz === "baz");

输出:

[get foobar]ctx[symbols.shadow].foobar=foo
[getFoobar]ctx[symbols.shadow].foobar=foo

然后我们访问服务代理的 baz 属性,会发现它返回的subContextfoobarservice.baz属性值,很明显这是将Trackerassociate属性设置为foobarservice 导致的结果。虽然服务代理的ctx属性被绑定为subContext,如果我们希望得到原始的Context呢?那也没问题,原始的上下文被作为影子上下文被附加到symbols.shadow 标识的属性上,上面的输出结果已经说明了一切。

上面我们演示的代理针对计算属性读取方法调用的拦截策略,我们下面来演示针对计算属性写入又会怎样。如下面的代码所示,我们直接修改了服务代理对象的foobar属性值,但是最终的结果是:两个Contextfoobar属性都没有改变。这说明了服务对象无法通过利用绑定的Context来修改它,它唯一可以修改Context的地方就是在构造函数中针对传入的Context进行操作。从这个意义上,这种Context绑定机制相当于为服务的执行提供了一个安全的基于沙箱的执行环境

serviceProxy.foobar = "qux";
console.assert(rootContext.foobar == "foo");
console.assert(subContext.foobar == "bar");

输出:

[set foobar]ctx[symbols.shadow].foobar=foo

1.2 noShadow = true

如果在创建FoobarSevice对象时将noShdow属性设置为true,其含义是:不要让原始上下文作为影子遮挡我,我们来看看演示程序执行结果是否会改变? 首先,基于当前Context的绑定不会改变,所以上面演示程序中设置的那些断言依然成立。

const service = new FoobarService(rootContext,true);
const serviceProxy = getTraceable(subContext, service);

console.assert(serviceProxy.foobar === "bar");
console.assert(serviceProxy.getFoobar() === "bar");
console.assert((serviceProxy as any).baz === "baz");

serviceProxy.foobar = "qux";
console.assert(rootContext.foobar == "foo");
console.assert(subContext.foobar == "bar");

输出:

[get foobar]ctx[symbols.shadow].foobar=foo
[getFoobar]ctx[symbols.shadow]=undefined
[set foobar]ctx[symbols.shadow].foobar=foo

唯一的不同发生服务对象的方法上,唯有当它执行的时候,作为执行上下文(this)提供的Context 不会添加影子上下文,这也符合设置noShadow的初衷。

2. 服务代理是如何被创建的

现在我们就来看看服务代理究竟是如何被创建出来的。不过在这之前,我们有必要先来探讨一下影子上下文的创建。通过上面的实例演示可知,所谓的影子上下文就是绑定在服务对象上的原始上下文。一般来说,原始的上下文会在服务类型的构造函数中提供,所以应用上下文一般指的就是服务实例被构建时提供的Context对象。

2.1 影子上下文的创建

Cordis中针的影子上下文通过如下的这个createShadow函数来创建,但它返回的并不是影子上下文本身,而是为了将影子代理附加到服务代理上重新生成的服务代理。四个参数说明如下:

  • ctx:传入getTraceable/createTraceable函数的当前Context对象(对于我们的例子来说就是subContext);
  • target:目标服务实例,就是传入getTraceable/createTraceable函数的value参数(对于我们的例子就是根据rootContext创建的FoobarService对象);
  • property:就是当前Context绑定的服务实例属性名称(对于我们的例子就是“ctx”);
  • receiver:作为当前执行上下文(this)的服务代理对象,也就是getTraceable/createTraceable函数返回的结果。
function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
  if (!property) return receiver
  const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value
  if (!origin) return receiver
  return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }))
}

function withProp(target: any, prop: string | symbol, value: any) {
  return withProps(target, Object.defineProperty(Object.create(null), prop, {
    value,
    writable: false,
  }))
}

export function withProps(target: any, props?: {}) {
  if (!props) return target
  return new Proxy(target, {
    get: (target, prop, receiver) => {
      if (prop in props && prop !== 'constructor') 
        return Reflect.get(props, prop, receiver)
      return Reflect.get(target, prop, receiver)
    },
    set: (target, prop, value, receiver) => {
      if (prop in props && prop !== 'constructor') 
          return Reflect.set(props, prop, value, receiver)
      return Reflect.set(target, prop, value, receiver)
    },
  })
}

了解了四个参数分别代表什么之后,我们来看看影子上下文是如何被创建的:

  • 如果没有通过Tranckerpoperty属性对当前Context绑定的属性进行显式设置,则直接返回当前的服务代理 receiver
  • Tranckerpoperty属性作为属性从目标服务对象中读取对应的值作为影子上下文;
  • 如果影子上下文不存在,还是直接返回当前的服务代理 receiver
  • 否则调用extend方法对当前Context进行扩展,扩展的结果就是在生成的子Context中添加一个由symbols.shadow 标识的影子上下文属性;
  • 调用withProp/withProps函数针对receiver创建一个新代理,使我们通过新代理对象访问绑定上下文的属性成员时(比如proxy.ctx),可以拿到携带影子上下文的当前Context。但是当我们对属性进行赋值的时候,实际上实在赋值操作作用在一个临时创建的对象上,而不是作用在target参数标识的服务对象上,以此来保证Context的不可改变。

2.2 createShadow函数的定义

如下所示的就是用来针对指定的Context对服务实例进行封装,使其成为一个能够自动跟踪指定Context的服务代理的createTraceable方法的定义。三个参数分别代表:当前的Context、服务实例和服务实例提供的Tracker对象。如果当前Context携带影子上下文,而trackernoShadow没有显式设置为true,则通过返回其原型丢弃影子上下文(这句代码的意图我还没搞明白)。

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow] && !tracker.noShadow) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
    get: (target, prop, receiver) => {...},
    set: (target, prop, value, receiver) => {...},,
    apply: (target, thisArg, args) => {...},
  })
  return proxy
}
2.2.1 服务属性的读取

如果value代表的服务对象是一个常规对象,当我们通过最终代理读取其属性时(包括字段、计算属)和方法),调用请求最终会被设置的代理处理器(ProxyHandler<T>)的get方法接管,具体执行逻辑如下所示:

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow] && !tracker.noShadow) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
    get: (target, prop, receiver) => {
      if (prop === symbols.original) return target
      if (prop === tracker.property) return ctx
      if (typeof prop === 'symbol') {
        return Reflect.get(target, prop, receiver)
      }
      if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
        return Reflect.get(ctx, `${tracker.associate}.${prop}`, 
        withProp(ctx, symbols.receiver, receiver))
      }
      let shadow: any, innerValue: any
      const desc = getPropertyDescriptor(target, prop)
      if (desc && 'value' in desc) {
        innerValue = desc.value
      } else {
        shadow = createShadow(ctx, target, tracker.property, receiver)
        innerValue = Reflect.get(target, prop, shadow)
      }
      const innerTracker = innerValue?.[symbols.tracker]
      if (innerTracker) {
        return createTraceable(ctx, innerValue, innerTracker)
      } else if (!tracker.noShadow && typeof innerValue === 'function') {
        shadow ??= createShadow(ctx, target, tracker.property, receiver)
        return createShadowMethod(ctx, innerValue, receiver, shadow)
      } else {
        return innerValue
      }
    },
    ...
  })
  return proxy
}

我们简单说执行流程:

  • 如果属性是一个symbols.original常量表示的Symbol,直接返回目标服务对象,意味着服务代理可以通过这种方式提供被它封装的原始对象;
  • 如果属性为tracker.property设置的绑定Context的属性,自然只需要返回当前Context就可以了;
  • 如果属性类型为Symbol,意味着这是绑定在原始服务对象上的一些特殊的元数据,需要直接由原始服务对象提供;
  • 如果设置了tracker.associate,并且当前Context具有一个名为{tracker.associate}.{prop}的属性,直接返回对应的属性值。还记得前面演示实例的这行代码console.assert((serviceProxy as any).baz === "baz")吗?
  • 如果目标服务具有指定名称对应的字段,则提取此字段另存为innerValue
  • 如果目标服务具有指定名称对应的计算属性(getter),由于涉及到函数的执行,此时需要调用createShadow函数读取影子上下文并生成携带此影子的新代理,然后将此代理作为执行上下文执行这个计算属性代表的函数,得到的属性值另存为innerValue
  • 这对返回innerValue,采用如下的策略生成最终的返回值:
    • 如果innerValue自身又包含Tracker,则递归地createTraceable方法,所以你会发现整个跟踪链的构建并不仅限于服务类型的成员,而是会持续递归下去,直到对象不提供Tracker对象;
    • 如果获取的属性是一个函数,也就是服务对象的方法,还是需要通过上面一样创建携带影子上下文的服务代理,并以此代理作为执行上下文执行方法对应的函数,这样在函数里面才有可能拿到影子上下文,所以它会调用createShadowMethod函数来创建并返回这样一个能够感知影子上下文的方法;
    • 其它情况下直接返回innerValue

用来创建能够感知到影子上下文方法的createShadowMethod定义很简单,只需要将提供的携带影子上下文的新的服务代理对象作为执行上下文(this)调用value代表的函数就可以了。值得一提的是,返回的结果会再次作为参数调用一遍getTraceable直至成为一个可跟踪当前Context的对象。

function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
  return new Proxy(value, {
    apply: (target, thisArg, args) => {
      if (thisArg === outer) thisArg = shadow
      return getTraceable(ctx, Reflect.apply(target, thisArg, args))
    },
  })
}
2.2.2 服务属性的设置

当我们需要通过服务代理进行属性赋值的时候,createTraceable方法创建代理时设置的处理器的set方法将会接管调用请求。

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow] && !tracker.noShadow) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
    set: (target, prop, value, receiver) => {
      if (prop === symbols.original) return false
      if (prop === tracker.property) return false
      if (typeof prop === 'symbol') {
        return Reflect.set(target, prop, value, receiver)
      }
      if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
        return Reflect.set(ctx, `${tracker.associate}.${prop}`, 
            value, withProp(ctx, symbols.receiver, receiver))
      }
      const shadow = createShadow(ctx, target, tracker.property, receiver)
      return Reflect.set(target, prop, value, shadow)
    },
  })
  return proxy
}

具体执行流程如下:

  • 如果属性为symbols.original,不做任何操作,不然的化岂不是将服务对象自身给覆盖了;
  • 如果属性为tracker.property设置的绑定Context的属性,不做任何操作,所以绑定在服务实例上的Context无法被覆盖;
  • 如果属性类型为Symbol,意味着这是绑定在原始服务对象上的一些特殊的元数据的设置,直接赋值给目标服务对象就可以了;
  • 如果设置了tracker.associate,并且当前Context具有一个名为{tracker.associate}.{prop}的属性,看起来是对当前Context的属性进行设置,由于设置的执行上下文,Context对象也不会被更新;
  • 调用createShadow创建包含影子上下文的服务代理,并以此为执行上下文对目标属性进行赋值。
2.2.4 当目标服务是函数

如果目标服务自身是一个函数,此时针对服务调用请求会由代理处理器的apply方法接管。从如下的代码,此时会进入applyTraceable函数。后者会实施双轨制:对于普通的函数,直接执行就可以了。如果服务对象上利用symbols.invoke常量注册了一个方法,意味着服务对象可以作为函数来执行,由于方法具有执行上下文的概念,所以需要使用当前创建的代理作为执行上下文才能将当前Context注入进去。

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow] && !tracker.noShadow) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
        apply: (target, thisArg, args) => {
      return applyTraceable(proxy, target, thisArg, args)
    },
  })
  return proxy
}

function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
  if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
  return value[symbols.invoke].apply(proxy, args)
}

3. GetTraceable函数又做了些什么

现在我们了解了用于创建可跟踪当前Context的服务对象是如何由createTraceable函数构造出来的,那么前面演示程序使用的GetTraceable函数又做了些什么呢?由于Context的自动跟踪功能是通过在服务对象上手工添加Tranker对象的方式开启的,所以getTraceable只有在验证待封装的对象的symbols.tracker属性包含此对象,才会调用createTraceable函数来创建服务代理。再次之前,如果指定的value不是一个对象(函数被视为一个对象),则直接返回value。除此之外,它还会执行剥离影子上下文的操作(这一步的意图,我目前也不是很清楚)。

export function getTraceable<T>(ctx: Context, value: T): T {
  if (!isObject(value)) return value
  if (Object.hasOwn(value, symbols.shadow)) {
    return Object.getPrototypeOf(value)
  }
  const tracker = value[symbols.tracker]
  if (!tracker) return value
  return createTraceable(ctx, value, tracker)
}

4. 作为代理的Context自身是如何被创建的

现在我们已经知道了,通过调用GetTraceable函数可以将一个常规的服务对象转换成可以自动跟踪当前Context的代理,那么这个函数的调用发生在何处呢?虽然我们可以手工创建服务调用,并调用此函数与指定的Context绑定,但是这样的编程体验无疑是很糟糕的。其实我们完全不需要去调用这个函数将自动上下文跟踪的能力引入服务,因为Context自身就是一个代理,它创建此代理提供的代理处理器会帮助我们完成这些工作。回到Context构造函数的定义,可以看出构造函数返回的对象正是当前对象 this 的代理,代理处理函数体现在定义在ReflectService的静态字段handler上。

export class Context {     
    constructor() {
    ...
    const self = new Proxy<this>(this, ReflectService.handler)
    ...
    return self
  }
}

我们依然按照前面介绍createTraceable创建代理的默认来看看ReflectService的静态属性handler提供的getset

4.1 上下文属性的读取

当我们从作为代理的Context对象读取它的某个属性是,设置的ProxyHandler<Context>的get方法会用来接管读取请求。

export class ReflectService {
  static handler: ProxyHandler<Context> = {
    get: (target, prop, ctx: Context) => {
      if (isSpecialProperty(prop)) {
        return Reflect.get(target, prop, ctx)
      }
      if (Reflect.has(target, prop)) {
        return getTraceable(ctx, Reflect.get(target, prop, ctx))
      }

      const error = new Error(`cannot get property "${prop}" without inject`)

      try {
        const def = target.reflect.props[prop]
        if (def?.type === 'accessor') {
          return def.get.call(ctx, ctx[symbols.receiver], error)
        }

        if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false)
        return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
          const key = target[symbols.isolate][prop]
          let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
          while (true) {
            const impl = fiber.store?.[prop]
            if (impl) return getTraceable(ctx, impl.value)
            if (prop in fiber.inject) {
              error.message = `cannot get required service "${prop}" in inactive context`
              throw error
            }
            if (!fiber.runtime) throw error
            if (fiber.parent[symbols.isolate][prop] !== key) throw error
            fiber = fiber.parent.fiber
          }
        })
      } catch (e: any) {
        throw e === error ? enhanceError(e) : e
      }
    }
  }
}

function isSpecialProperty(prop: string | symbol): prop is symbol {
  return typeof prop === 'symbol'
    || RESERVED_WORDS.includes(prop)
    || parseInt(prop).toString() === prop
    || prop.startsWith('_')
}

具体流程如下:

  • 对于特殊的属性(通过上述的isSpecialProperty函数来判断)读取,不需要实施任何拦截操作,直接放行由原始的Context对象提供即可;
  • 如果属性定义在目标Context中,直接提取出来交给getTraceable函数实施转换即可;
  • 我们可以利用ReflectServiceaccessor方法注册一个计算属性,如果指定的属性对应一个这样的计算属性,直接以当前Context计算并返回属性值;
  • 如果当前Context的fiber对应的runtime不存在,意味着当前Context并不存在注册的插件,换句话说当前Context是一个根。由于已经处于最顶端,只需调用ReflectServiceget方法返回的结果就可以了;
  • 当我们调用ReflectServiceprovide方法进行服务注册的时候,它会得到当前的Context,并将注册的服务保存在它对应Fiber对象的store字典中。所以对于非根Context,会通过属性名试着从当前Fiber中提取对应的服务对象,如果存在则提交给getTraceable方法实施转换;然后按照这个对象不断向上追溯。

上述的针对Context不断向上追溯攀爬的过程中,有如下两个细节:

  • 如果当前的属性名称在某个Fiberinject列表中,意味着当前读取的属性为该Fiber绑定插件注入的依赖服务之一,此时它会直接抛出异常。为什么它可以在不需要继续向上追溯的前提下就能判断依赖服务没有激活呢?不会包含在上面的Context中吗? 这是因为一旦某个服务激活,Cordis就会立即将它推送给所有将它作为注入服务的插件,并保存在绑定Fiberstore字典中;
  • 整个流程是以waterfall形式发送一个名为internal/get的事件的方式进行的,具体的读取操作体现为调用waterfall方法提供的回调(next),意味着某一项加载器可以注册此事件以一种Lazy loading的方式按需提供服务。

4.2 上下文属性的设置

当我们试图去设置Context代理的某个属性时,提供的ProxyHandler<Context>的set方法被调用,具体的执行流程如下:

  • 对于特殊的属性(通过上述的isSpecialProperty函数来判断)读取,不需要实施任何拦截操作,直接放行赋值给原始Context对象;
  • 如果属性并没有包含在ReflectService的props字段中,意味着并未通过provide方法注册过,直接排除异常;
  • 如果属性对应通过ReflectService的accessor方法设置的计算属性,直接执行对应的set方法;
  • waterfall形式发送一个名为internal/set的事件并提供属性设置的回调(ctx.reflect.set(prop, value, error)),相关的组件可以通过监听此事件阻止赋值操作。
export class ReflectService {
  static handler: ProxyHandler<Context> = {    
    set: (target, prop, value, ctx: Context) => {
      if (isSpecialProperty(prop)) {
        return Reflect.set(target, prop, value, ctx)
      }

      const error = new Error(`cannot set property "${prop}" without provide`)
      const def = target.reflect.props[prop]
      if (!def) {
        if (!ctx.fiber.runtime) return Reflect.set(target, prop, value, ctx)
        throw enhanceError(error)
      }

      try {
        if (def.type === 'accessor') {
          if (!def.set) return false
          return def.set.call(ctx, value, ctx[symbols.receiver], error)
        }

        return ctx.events.waterfall('internal/set', ctx, prop, value, error, () => {
          return ctx.reflect.set(prop, value, error)
        })
      } catch (e: any) {
        throw e === error ? enhanceError(e) : e
      }
    }
  }
}
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐