ByteNoteByteNote
DeepSeek Harness 用户配置子系统解析
字

字节笔记本

2026年10月6日 · 约 6 分钟读完

DeepSeek Harness 用户配置子系统解析

API中转
¥120

跑多插件的 agent 应用,配置是绕不开的问题:每个插件都有自己的参数,用户要能改,配置界面要能渲染,密钥不能泄露,多个界面同时写还不能互相覆盖。DeepSeek Harness 在 GitHub 上以 MIT 协议开源,它把 agent 应用的每个能力都做成插件,底层靠 Cordis 框架组合,它的 settings 接缝(dsh-settings 包)就是为这套配置问题给出的答卷。本文基于该项目的用户设置文档,拆解其中的关键设计。

dsh-settings 三层解析与注册:fiber 绑定、scope 四个动作、三层合并

一份文档,按命名空间分节

整个系统只维护一份用户拥有的配置文档,文档按命名空间分成一节一节。命名空间是一个品牌化的 id,构造时强制小写 kebab-case,目的是让插件自己的设置段和其他在包与进程之间传递的 id 在类型上不可混用。文件类提供者负责存这份原始文档,并把外部编辑推回系统;消费方插件注册自己的模式,然后读取或观察解析后的值。分工上有一条值得注意的边界:组合配置留在 cordis.yml,命名空间里只放用户可编辑的子集,两层互不打架。

三层合并,用户层赢在最后

每个命名空间的最终值按固定顺序解析:先取模式默认值,再叠加注册方声明的组合层 base,最后叠加用户节。base 让插件作者给出一组比默认值更贴近场景的预置,用户层永远覆盖在前两层之上。解析出来的值是深冻结快照,拿到手也改不动,想变值只能走写入路径。

注册即生命周期

register 把 schemastery 模式绑定到命名空间,注册动作本身是调用插件所在 fiber 上的一个 effect:fiber 销毁,命名空间和挂在它上面的观察者一起移除,重复注册会直接报错。注册时还可以带三个选项。base 是组合层的兜底值。applies 声明生效时机,取值 live 或 restart,它只是给配置界面的提示而不是机制,声明为 restart 的插件根本不会监听变更,值在构造时读一次,界面可以据此把未生效的修改标出来。validate 是模式表达不了的跨字段约束,比如一个字段的合法性依赖另一个字段。

validate 的位置选得很讲究:它在模式放行之后运行,看到的就是插件最终会拿到的值,抛错就拒绝产生这个值的那次写入,而不是把一个会让插件静默失效的配置存进文档。文档里举的实例是某个模型插件用它拒收自己服务不了的 provider 配置,避免存进去之后整个命名空间的路由全被带崩。插件已注册时,外部编辑进来的坏值会让命名空间保留上一个好值并告警,与模式校验失败同等待遇;注册那一刻还没有好值可保,所以直接拒绝注册。

Owner 手上的四个动作

注册返回的 scope 是面向属主的句柄,提供四个动作。get 读当前解析值。update 把稀疏补丁合并进用户层,永远不会写进 base,补丁必须是 JSON 兼容数据,不兼容的值会在落盘前带着路径被拒绝。replace 整节替换用户节,是删除与重置的路径,替换里缺掉的键重新回落到 base 和模式默认值,replace 空对象等于全部重置。同一命名空间的写入按调用顺序串行化,并发调用各自排在前一次提交的结果之上。

watch 注册观察回调,规矩也很细:同一个回调的多次调用异步逐个执行,严格按提交顺序;回调同步抛错或异步 reject 都会被遏制并记录,不会波及其他观察者;注销之后,已排队的那次调用被跳过,已开始的那次仍然会跑完,服务销毁会等它。

dsh-settings 写入路径与密钥安全:校验、持久化、事件与脱敏

描述符与密钥脱敏

describe 把所有已注册命名空间序列化给配置界面:模式的 toJSON 信封用来渲染表单,解析值用来填充,分离给出的 base 层和 user 层让表单能靠字段是否在 user 层出现来判断用户有没有覆盖过。安全上的硬约束是:一切走网络的表面必须开 redactSecrets,它把标记为密钥的字段从解析值、base、user 三层里全部剥掉,只枚举出它们的位置,页面因此能渲染只写输入框,却从没收到过密钥本身。不加脱敏的原样输出只允许在同进程的配置界面里用。

这条约束带来一个直接推论:只拿到脱敏描述符的调用方无法安全重建整节,如果用整节 replace 回写,所有从未回传过的密钥会被静默删掉。所以局部修改和删除走路径操作,set 和 unset 按路径点名要动的字段,操作应用在写入队列排到它时的最新用户节上,调用方既不必复述自己没碰过的字段,也删不掉自己没见过的字段。整节 replace 保留给真正的重置场景。

版本号守并发

每个描述符带一个基于原始用户节的单调递增 revision。写回时可以带上 expectedRevision,一旦这节配置已经被人先改过,版本对不上,这次写入就被拒绝并报冲突错误,而不是盖掉先落地的修改。乐观锁加串行化队列,两个配置页面同时改一节配置也不会互相吞掉。

两个事件,两种粒度

提交后的变更统一发 settings/updated,带命名空间、新值、旧值和来源,来源区分进程内写入和提供者推送的外部编辑,解析值深相等时不发。监听器同步抛错或异步 reject 都被遏制记录,只有不变量类失败会在所有监听器跑完后重抛,也因此这类监听器不能写成异步函数。另一个事件 settings/document-updated 粒度更粗:原始用户节变了就发,哪怕解析值没变。它专门给配置界面用,界面需要知道某个字段从继承变成了覆盖,值一样含义却不同,也需要知道自己手里的版本号已经过期。

可以搬走的设计

这套设计里有几件事不依赖 Cordis 也能照搬:配置分三层合并,默认值、预置、用户各管一段;校验放在写入时,坏值根本不落文档;密钥在接口面单向脱敏,只给位置不给内容,删除改用路径操作而不是整节回写;写回带版本号做乐观并发。任何要做多插件配置界面的项目,都可以从这份文档里直接抄作业。

相关文章

分享: