[DeepSeek Harness插件内核-02]服务的注册与消费

DeepSeek Harness插件内核-01:DeepSeek Harness三种插件形式中介绍了Cordis插件的三种定义方式,由于演示的插件功能简单,所以直接将所有的功能实现在作为插件的函数或者对象上。真正的插件开发中,我们都倾向于将业务功能定义成服务并注册到Context这个依赖注入容器上,任何依赖此功能的插件都可以从容器中提取并消费所需的服务,从而达到功能复用的目的。本篇文章将会通过一系列简单的实例来演示服务的定义、注册和消费。

1. 将服务注册到Context上

我们依然使用DeepSeek Harness插件内核-01:DeepSeek Harness三种插件形式中演示的例子:注册一个插件输出一条问候语(比如Good morning|afternoon|evening)。前面我们使用配置的方式来提供时间部分的内容(morning|afternoon|evening),现在我们采用注册服务的形式来提供此内容。如下面的代码所示,我们在main函数中根据用户输入的内容创建一个timeOfDayService对象,提供的时间由它的value属性表示。我们从Context的reflect属性得到用于提供反射相关操作的ReflectService对象,并调用其provde方法将这个对象以服务的形式注册到Context上。

import { Context,Service } from '@deepseek-ai/cordis'
import * as readline from "readline"

function greet(ctx: Context){
    const timeOfDayService =  (ctx as any)["timeOfDay"] as {value:string};
    console.log(`Good, ${timeOfDayService.value}`);
}

async function main() {
  const ctx = new Context();
  const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout});    

  rl.question("Enter time of day:", (answer) => {      
    const timeOfDayService = {
        value: answer
    };
    ctx.reflect.provide("timeOfDay",timeOfDayService, ()=>true);
    ctx.registry.plugin(greet);
    });
};

main();

调用provide方法出了传入作为服务对象之外,还需要指定额外两个参数:分别是表示注册名称的timeOfDay和用来服务可用性的函数。完成服务注册之后,相当于会在Context对象上以反射的方式额外添加一个针对注册服务对象的属性,对应的属性名就是服务的注册名称。所以表示插件的greet函数可以直接从传入的Context对象中提取这个用于提供问候时间的服务,并从其value属性得到所需的时间,并完成最后问候语的输出:

Enter time of day:morning
Good, morning

2. 在Context接口中为注册服务声明一个属性

在利用ReflectServiceprovide方法将服务实例以指定的名称注册到Context对象上之后,虽然我们可以按照greet函数所示方式根据注册名称将服务实例提取出来,但是这种方式未免过于繁琐和丑陋了一些,最好的方式是直接为Context接口声明一个针对注册服务的属性。

declare module '@deepseek-ai/cordis' {
  interface Context {
    timeOfDay: {value:string}
  }
}
function greet(ctx: Context){
    console.log(`Good, ${ctx.timeOfDay.value}`);
}

如上面的代码所示,我们利用declare module语句为Context接口添加了一个针对注册服务的属性声明,属性名称为服务注册的名称。这样在greet插件函数中我们就可以直接使用这个属性得到强类型的注册服务了。

3. 扩展Service基类定义服务

为了解释服务注册的本质,我们在上面的演示中创建了一个单纯的对象作为服务,并手工调用ReflectServiceprovide方法完成服务注册。实际上Cordis为服务提供了一个名为Service的基类,我们通过直接扩展这个基类会使一切变得更简单。如下面的演示程序所示,我们定义了一个扩展Service的服务类型TimeOfDayService,并利用其value属性来提供时间。构造函数定义了两个参数,分别是Context对象和提供的时间内容,我们调用基类的构造函数super(ctx, "timeOfDay")自动完成服务的注册。我们依然使用declare module语句为Context为注册的服务针对注册名称timeOfDay声明一个属性,声明的类型自然就是服务类型TimeOfDayService

import { Context,Service } from '@deepseek-ai/cordis'
import * as readline from "readline"

class TimeOfDayService extends Service{
    value: string;
    constructor(ctx:Context, value: string){
        super(ctx, "timeOfDay");
        this.value = value;
    }
}

declare module '@deepseek-ai/cordis' {
  interface Context {
    timeOfDay: TimeOfDayService
  }
}
function greet(ctx: Context){
    console.log(`Good, ${ctx.timeOfDay.value}`);
}

async function main() {
  const ctx = new Context();
  const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout});    

  rl.question("Enter time of day:", (answer) => {      
    new TimeOfDayService(ctx, answer);
    ctx.registry.plugin(greet);
    });
};

main();

在main函数中,我们只需要根据用户输入完成TimeOfDayService的对象的创建就可以了。启动程序并指定相应的输入,注册的greet插件依然可以输出我们希望的内容(如下所示)。究竟作为基类的Service内部做了些什么,我们会在本系列后续文章提供对它的详细介绍。

Enter time of day:morning
Good, morning

4. 将服务注入插件

对于上面的演示程序,我们总是在注册作为服务消费者的greet插件之前就完成了针对依赖服务的注册。但是真实的场景是:

  • 依赖服务可能在插件注册之后才注册;
  • 依赖服务随着作为提供者的插件卸载而被卸载。

为了确保作为服务消费者插件的正常执行,需要在插件和依赖服务之间构建一种依赖关系,并提供一种实施检测服务可用性状态的机制。我们可以将这种机制理解为将依赖服务**注入(inject)**到作为服务消费者的插件之中。DeepSeek Harness插件内核-01:DeepSeek Harness三种插件形式中介绍的三种插件接口都是针对如下这个Plugin.Base<T>接口的扩展,其可缺省的inject属性表示的就是注入的服务名称。

export namespace Plugin {
  export interface Base<T = any> {
    name?: string
    Config?: StandardSchemaV1<any, T>
    inject?: Inject
    provide?: string | string[]
    intercept?: Dict<boolean>
  }
}
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }

在如下的演示程序中,我们将针对TimeOfDayService的注册实现在timeOfDayPlugin这个单独的插件中。在main函数中,我们采用对象形式先完成了greet插件的注册,并在插件对象的inject属性上提供了注入的服务名称timeOfDay。然后再等待5秒钟后再注册提供依赖服务的timeOfDayPlugin插件。

import { Context,Service } from '@deepseek-ai/cordis'
import * as readline from "readline"

class TimeOfDayService extends Service{
    value: string;
    constructor(ctx:Context, value: string){
        super(ctx, "timeOfDay");
        this.value = value;
    }
}

function timeOfDayPlugin(ctx: Context, config: {timeOfDay: string}){
    new TimeOfDayService(ctx, config.timeOfDay);
    console.log("TimeOfDayService has been registed.")
}

declare module '@deepseek-ai/cordis' {
  interface Context {
    timeOfDay: {value:string}
  }
}

async function main() {
  const ctx = new Context();
  const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout}); 

  rl.question("Enter time of day:", (answer) => {   
    ctx.registry.plugin({
        apply: function(ctx: Context, config: any){ console.log(`Good, ${ctx.timeOfDay.value}`)}, 
        inject:["timeOfDay"]});
    setTimeout(()=>ctx.registry.plugin(timeOfDayPlugin, {timeOfDay:answer}), 5000)
    ;
    });
};

main();

输出如下的输出可以看出,虽然greet插件一开始就被注册,但是由于注入服务的确实,它并不会贸然执行。Cordis内部利用事件总线向插件通知所需服务的可用状态,所以当我们通过注册timeOfDayPlugin插件完成服务注册后, greet插件会立即执行。

Enter time of day:morning
TimeOfDayService has been registed.
Good, morning

其实RegistryService为我们定义了如下的inject方法来完成基于服务注入的插件注册,可用看出最终还是像上面的演示程序一样调用plugin方法以对象形式进行插件的注册。

export class RegistryService {
  inject(inject: Inject, callback: Plugin.Function<void>) {
    return this.plugin({ inject, apply: callback, name: callback.name })
  }
}

所以演示程序中针对greet插件的注册可用替换成如下的形式:

async function main() {
  const ctx = new Context();
  const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout}); 

  rl.question("Enter time of day:", (answer) => {   
    ctx.registry.inject(["timeOfDay"], (ctx, _) => console.log(`Good, ${ctx.timeOfDay.value}`));
    setTimeout(()=>ctx.registry.plugin(timeOfDayPlugin, {timeOfDay:answer}), 5000)
    ;
    });
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值