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接口中为注册服务声明一个属性
在利用ReflectService的provide方法将服务实例以指定的名称注册到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基类定义服务
为了解释服务注册的本质,我们在上面的演示中创建了一个单纯的对象作为服务,并手工调用ReflectService的provide方法完成服务注册。实际上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)
;
});
};

57

被折叠的 条评论
为什么被折叠?



