Skip to content

dsh 中的每个插件都是可配置的,而配置是一套一等公民、由 schema 驱动的系统。settings 服务ctx.settingspackages/settings/settings)是面向用户的那一半:插件注册一个命名空间 schema,provider 持有一份按命名空间分节的原始文档,而消费者读取的是按 schema 默认值 → 注册者的组合 base → 用户文档分节 逐层叠加后的 解析后的 值。上游的 docs/config-catalog.md(由 scripts/gen-config-catalog.ts 生成)是组合中每个配置字段的机器可读目录。

角色
@deepseek-ai/dsh-settings服务定义:ctx.settingsSettingsScope、分层解析
@deepseek-ai/dsh-settings-file基于文件的 provider(harness 主目录中的 settings.yaml
@deepseek-ai/schemastery(vendored)用于构建命名空间的 schema 校验库

分层:schema 默认值 → base → 用户

命名空间按顺序合并三层来解析其值:

  1. Schema 默认值 —— 由插件的 schema 声明;
  2. 组合 base —— 注册者的 cordis.yml 条目配置子集(即 bundle 随附的内容);
  3. 用户文档 —— provider 存储的分节(即用户写的内容)。

没有挂载 provider 时,对消费者而言一切照旧:它们继续单独解析条目配置,因此每个组合在有无 settings 的情况下都能正常工作。这是一个刻意为之的性质——settings 是 附加项,从不构成硬性要求。

服务 API

取自 packages/settings/settings/README.md 的核心接口:

  • documentPath —— provider 的用户可编辑文件的绝对路径;对于非文件型 provider 则为 undefined。宿主配置适配器据此推导可用性;浏览器协议只暴露布尔能力,从不暴露文件系统目标。
  • prepareDocument() —— 在让文档准备好供原生编辑器打开后,返回该路径。
  • register(ns, schema, { base?, applies? }) —— 返回所属的 SettingsScopeget / watch / update)。注册是对调用插件 fiber 的一次效果:处置该 fiber 会移除该命名空间及其观察者。被 schema 拒绝的已存分节会使注册本身失败;重复的命名空间会大声报错。
  • describe(options?) —— 每个命名空间一份描述符:schema.toJSON() 信封、解析后的值、分离的 base / user 层、applies。字段出现在 user 中即标志着它被用户覆盖。describe({ redactSecrets: true }) 会从每一层中剔除 role('secret') 字段,并新增 secrets 槽位列表({ path, set });每个线路面都必须传它
  • get(ns) —— 解析后的值;未注册时返回 undefined
  • update(ns, patch) —— 将普通对象补丁深合并到 仅限用户分节(绝不涉及 base),校验解析后的候选值,经 provider 持久化,然后提交。补丁只能包含 JSON 兼容的数据——DateMapBigInt、非有限数字或循环引用会以其 $ 为根的路径被拒绝,此时尚未持久化任何内容。
  • replace(ns, section) —— 整体设置用户分节:这是刻意设计的重置操作(replace({}) 会重新继承 base 与 schema 默认值)。
  • mutate(ns, ops) —— 应用有序的 { op: 'set' | 'unset', path } 编辑。这是任何持有 不完整 视图的调用方的移除路径:配置 UI 读取的是脱敏后的描述符,因此若由它重建分节并整体替换,就会删除线路从未返回的每个机密;而一条 op 只指名它真正想改的那一个字段。
  • 每次写入都接受可选的 expectedRevision;过期的写入会以 SettingsConflictErrorcode: 'SETTINGS_CONFLICT')拒绝,而不是静默覆盖先落地的写入者。

修订号与并发

每份描述符都携带命名空间的 revision,这是针对其 原始 分节的单调递增计数器。写入队列对写入排序,但其自身无法区分一个全新的写入者与一个持有陈旧快照的写入者——这正是 expectedRevision 的用途。对同一命名空间的写入按调用顺序串行化;只读 provider(writable: false)会拒绝每一次写入。

可观测性:两个事件

  • settings/updated (ns, next, prev, source) —— 每次提交后触发;sourceupdate(进程内写入)或 provider(外部更改)。对于深相等(deep-equal)的解析值永不触发。
  • settings/document-updated (ns, revision) —— 只要 原始 用户分节发生变化就触发,无论解析后的值是否变化。配置界面需要用到它:存储一个与组合 base 相等的覆盖值虽然不会改变解析后的值,但会改变文档所记录的内容,并推进每个已打开的编辑器所持有的修订号。

观察者在每次提交后收到 (next, prev),按提交顺序一次一个,并带有包含性保证:一次缓慢的过期调用绝不会在一次更新的调用之后才应用,而抛出异常的监听器也绝不会饿死其余监听器。解析后的值是深冻结的快照。

provider 契约

子类实现 writableload()persist(ns, section),可选地为一份本地用户可编辑文件覆盖 documentPath / prepareDocument(),并通过受保护的 publish(doc) 推送外部观察到的文档。发布时,每个已注册的命名空间独立地重新解析:无效分节会保留该命名空间的最后一个良好值并发出警告——热重载绝不会拖垮进程,而启动时与注册时的校验则会大声失败。

随附的文件 provider(packages/settings/settings-file)在 harness 主目录中持久化 settings.yaml(参见 packages/util/home-paths)。

配置目录

docs/config-catalog.md 是生成的、完整的配置字段目录——针对随附的每个组合中的每一行插件,附带 schema、默认值与嵌套关系。它由 scripts/gen-config-catalog.ts(TypeScript AST + Schemastery)生成,而非 由 Typert 生成(这一区别在 Typert: 类型生成器 中有文档说明)。当某个插件新增配置字段时,目录是首先应去核对字段含义及其默认值来源的地方。

settings 与 UI 的交汇

浏览器通过 schema-form 系统渲染命名空间 schema(参见 Schema Form):describe() 的输出驱动设置界面,update()/mutate() 持久化用户更改,而 document-updated 让已打开的编辑器保持同步。UI 永远看不到机密——脱敏后的描述符对此作出了保证。

关键源文件

仓库相对路径提供内容
packages/settings/settings/src/服务定义、SettingsScope、解析引擎
packages/settings/settings-file/src/基于文件的 provider(settings.yaml
scripts/gen-config-catalog.ts配置目录生成器
docs/config-catalog.md生成的目录(双语)
vendor/schemastery/Schema 库

延伸阅读