@deepseek-ai/dsh-settings包提供settings服务是DSH中用于管理运行时配置的底层核心服务,专门负责系统参数的命名空间注册、分层解析、热更新持久化以及变更检测。@deepseek-ai/dsh-settings-file进一步提供了基于文件的配置系统,本篇文章将通过几个简单的实例来提供DSH基于配置的编程体验。
1. DSH的settings服务
DSH利用settings这个基础服务为所有的插件提供结构化配置,该服务返回一个SettingsProvider对象。如下面的代码所示,这是一个抽象类,由@deepseek-ai/dsh-settings包提供。派生于它的FileSettingsProvider实现了基于指定文件(支持yaml和json)的配置系统,对应的包为@deepseek-ai/dsh-settings-file。
exportabstractclassSettingsProviderextendsService{constructor(ctx:Context){super(ctx,'settings')}}exportclassFileSettingsProviderextendsSettingsProvider{constructor(ctx:Context,publicconfig:Config){super(ctx)...}}exportinterfaceConfig{path?:stringdshHome?:stringwatch?:booleandebounceMs?:number}和很多基础服务一样,FileSettingsProvider以插件的方式自身注册为基础服务。插件对应的配置接口Config定义了如下的配置选项:
- path:设置文档的完整路径。若省略,则使用默认的路径
{dshHome}/settings.yaml; - dshHome:当path未提供时使用的harness 主目录,默认取环境变量
$DSH_HOME; - watch:是否监听配置文件并热发布外部修改,默认true。开启后,手动编辑文件也能让设置立即生效;
- debounceMs:监听器的写入稳定窗口(毫秒),默认100。文件系统事件常会连续触发多次,用这个窗口合并短时间内的多次写入,避免频繁重载。
2. 配置的注册
register是SettingsProvider的核心方法,插件通过它声明自己要用哪块配置、配置长什么样,并拿回一个可读写该配置的作用域对象。泛型参数T表示配置对象的类型。
exportabstractclassSettingsProviderextendsService{register<constNamespaceextendsstring,T>(ns:Namespace&SettingsNamespaceInput<Namespace>,schema:z<T>,options?:SettingsRegisterOptions<T>,):SettingsScope<T>}exportinterfaceSettingsRegisterOptions<T>{base?:Partial<T>applies?:SettingsApplies validate?:(value:T)=>void}exporttypeSettingsApplies='live'|'restart'register方法定义了如下三个参数:
- ns:一个字符串,作为该插件配置在全局设置文档中的唯一标识;
- schema:一个由
@deepseek-ai/schemastery提供的z<T>对象,描述配置的结构、字段类型与默认值。服务用它来校验写入数据、填充缺省值,并推导配置对象的类型; - options:
SettingsRegisterOptions<T>,用于控制注册行为的额外选项,包括提供的基础配置、生效或可见和验证操作。
register方法返回的SettingsScope<T>对象代表注册配置的作用域,是插件读写自身配置的入口。四个方法覆盖了配置的读取、更新的监听、增量修改和整体替换四个操作。
exportinterfaceSettingsScope<T>{get():Twatch(callback:(next:T,prev:T)=>void|Promise<void>):()=>voidupdate(patch:object):Promise<void>replace(section:object):Promise<void>}2.1 配置的读取
我们通过如下的程序来演示如何调用上述的register方法基于指定的命名空间注册一组结构化的配置。简单起见,我们的配置对象只包含foo和bar两个字符串类型的成员,通过FoobarConfig接口来表示。声明为z<FoobarConfig>类型的Config通过调用z.object方法为它创建的Schema。我们在创建的根Context中注册了FileSettingsProvider插件,并设置了配置文件的路径(./explor/foobar.json)。
import{Context}from'@deepseek-ai/cordis'import{FileSettingsProvider}from'@deepseek-ai/dsh-settings-file'importzfrom'@deepseek-ai/schemastery'interfaceFoobarConfig{foo:stringbar:string}constConfig:z<FoobarConfig>=z.object({foo:z.string(),bar:z.string(),})constctx=newContext()ctx.plugin(FileSettingsProvider,{path:"./explor/foobar.json"})ctx.inject(["settings"],ctx=>{constscope=ctx.settings.register("foobar",Config)constconfig=scope.get()console.log(`initial settings:${JSON.stringify(config)}`)})在另一个调用inject方法注册的插件中,我们调用ctx.settings.register方法完成了针对Config的注册,对应的命名空间被设置为foobar。在得到返回的SettingsScope<Config>对象后,我们调用get方法得到配置对象,并将其序列化成JSON后输出。配置文件(./explor/foobar.json)的内容如下:
{"foobar":{"foo":"111","bar":"111"}}程序之后,输出的配置对象将与配置文件的内容匹配:
initial settings: {"foo":"111","bar":"111"}2.2 配置的增量更新和替换
下面的程序进一步演示了通过调用SettingsScope<T>的update和replace方法对配置的增量更新和整体替换,对应的操作最终修改配置文件的内容。如下面的代码所示,我们在调用register方法并得到对应的SettingsScope<Config>对象后,先后调用了update和replace方法,随后读取并输出了配置文件的内容。
import{Context}from'@deepseek-ai/cordis'import{FileSettingsProvider}from'@deepseek-ai/dsh-settings-file'importzfrom'@deepseek-ai/schemastery'import{readFile}from'node:fs/promises'interfaceFoobarConfig{foo:stringbar:string}constConfig:z<FoobarConfig>=z.object({foo:z.string(),bar:z.string(),})constctx=newContext()ctx.plugin(FileSettingsProvider,{path:"./explor/foobar.json"})ctx.inject(["settings"],asyncctx=>{constscope=ctx.settings.register("foobar",Config)awaitscope.update({foo:"222"})console.log(awaitreadFile("./explor/foobar.json","utf-8"))awaitscope.replace({foo:"333",bar:"333"})console.log(awaitreadFile("./explor/foobar.json","utf-8"))})输出的两端JSON:
{"foobar":{"foo":"222","bar":"111"}}{"foobar":{"foo":"333","bar":"333"}}2.3 监控配置文件的更新
通过调用SettingsScope<T>的watch方法可以监控配置文件的更新,在重新加载配置对象之后,会将更新前后的配置作为参数调用指定的回调函数。在如下的演示程序中,我们在调用register方法并得到对应的SettingsScope<Config>对象后,先后调用了watch注册了一个回调函数将检测到的配置更新输出来。为了让注册的插件不推出,我们调用effect方法进行了长时间的等待。
import{Context}from'@deepseek-ai/cordis'import{FileSettingsProvider}from'@deepseek-ai/dsh-settings-file'importzfrom'@deepseek-ai/schemastery'interfaceFoobarConfig{foo:stringbar:string}constConfig:z<FoobarConfig>=z.object({foo:z.string(),bar:z.string(),})constctx=newContext()ctx.plugin(FileSettingsProvider,{path:"./explor/foobar.json"})ctx.inject(["settings"],asyncctx=>{constscope=ctx.settings.register("foobar",Config)scope.watch((next,prev)=>{console.log(`detect settings changes: previous:${JSON.stringify(prev)}next:${JSON.stringify(next)}`)})ctx.effect(()=>{consttimer=setInterval(()=>{},1000*60*60)return()=>clearInterval(timer)})})程序启动后,我们可以手工修改位置文件的内容,我们的程序会实时将配置更新输出来:
detect settings changes: previous: {"foo":"333","bar":"333"} next: {"foo":"444","bar":"333"} detect settings changes: previous: {"foo":"444","bar":"333"} next: {"foo":"444","bar":"555"}3. 另一个更有用的方法
其实我们使用settings服务很少会使用到register方法,更多的时候使用的是它的installSection方法。如果我们需要注册一个持续、不间断工作的插件或者服务,我们应该直接注入settings服务,因为这样会导致settings服务不可用的时候,自身也会被卸载掉。我们希望的配置消费形式是:
- 如果
settings服务上线,就使用它提供的配置; - 否则使用预设的配置兜底。
installSection方法定义如下。由于它内部也会调用register方法完成配置的注册,所以也需要提供命名空间和Schema。owner对应的Context与作为消费者的插件或者服务绑定,entry参数提供的就是那个兜底的配置。hooks提供的SettingsSectionHooks<T>对象包含三个构造函数,分别用来提供配置、监控更新和验证配置。
exportabstractclassSettingsProviderextendsService{installSection<constNamespaceextendsstring,T>(owner:Context,ns:Namespace&SettingsNamespaceInput<Namespace>,schema:z<T>,entry:T,hooks:SettingsSectionHooks<T>,):void}exportinterfaceSettingsSectionHooks<T>{setSource(current:()=>T):voidonChange():voidvalidate?:(value:T)=>void}在如下这个演示程序中,我们注册的FoobarService用来模拟哪个不直接依赖于settings的服务,它的getConfig方法用来提供当前的配置。它的构造函数利用fallback参数来提供兜底的配置,并用它来初始化getConfig方法。我们在构造函数中通过调用inject方法注册了一个子插件,并在其中调用settings服务的installSection方法,最终的目的就是利用构造函数setSource得到用来提取配置的函数,并用来作为FoobarService服务的getConfig方法。
import{Context,Service}from'@deepseek-ai/cordis'import{FileSettingsProvider}from'@deepseek-ai/dsh-settings-file'importzfrom'@deepseek-ai/schemastery'import{writeFile}from'node:fs/promises'interfaceFoobarConfig{foo:stringbar:string}constConfig:z<FoobarConfig>=z.object({foo:z.string(),bar:z.string(),})classFoobarServiceextendsService{getConfig:()=>FoobarConfigconstructor(ctx:Context,fallback:FoobarConfig){super(ctx,"foobar")this.getConfig=()=>fallback ctx.inject(["settings"],settingsCtx=>{settingsCtx.settings.installSection(ctx,"foobar",Config,fallback,{setSource:current=>this.getConfig=current,onChange:()=>{}})})}}declaremodule'@deepseek-ai/cordis'{interfaceContext{foobar:FoobarService}}awaitwriteFile("./explor/foobar.json",JSON.stringify({foobar:{foo:"111",bar:"111"}}),'utf8')constctx=newContext()constsettingsFiber=ctx.plugin(FileSettingsProvider,{path:"./explor/foobar.json"})awaitsettingsFiber.await()awaitctx.plugin(FoobarService,{foo:"000",bar:"000"}).await()ctx.inject(["foobar"],asyncctx=>{console.log("setings service is available:")console.log(ctx.foobar.getConfig())awaitsettingsFiber.dispose()console.log("setings service is not available:")console.log(ctx.foobar.getConfig())})程序开启执行的时候通过写入配置文件(./explor/foobar.json)的形式将配置设置为{ foo: '111', bar: '111' },然后以此配置文件的路径注册了FileSettingsProvider插件,并得到对应的Fiber对象。在注册Foobarservice的时候,我们将兜底的配置设置为{ foo: '000', bar: '000' }。
我们在注册的插件中演示FoobarService针对配置的兜底功能。如代码所示,我们在settings可用的条件下调用其getConfig方法,并输出返回的配置。然后我们调用settings服务所在Fiber的dispose方法,其目的是将settings服务卸载掉,然后再次输出FoobarService的配置。可以看出两次输出是不同的。
setings service is available: { foo: '111', bar: '111' } setings service is not available: { foo: '000', bar: '000' }