DSH plugin 接 StorageBackend:DomainSpec、StorageForms 与变更事件
DSH plugin 的存储子系统有一条清晰分界:枢纽只做汇合,后端拥有介质,数据形式拥有语义。 所以 ctx.storage 里没有任何 IO——它是「名称 → 后端」的表;真正的写盘发生在 dsh-storage-json(整文件原子重发)或 dsh-storage-sqlite(一行一文档)里;而产品包与插件一律通过 ctx.storageDomain 的类型化 API 访问数据,绝不直接碰后端。这套划分带来两个直接结论:选哪个后端是消费方自己的路由配置,不是枢纽的全局选择;写入必须先落盘再更新内存,最后才发 domain/changed。
DSH plugin 的存储枢纽:后端管介质,数据形式管语义
一句话结论:ctx.storage 是汇合点而不是存储本体;它只登记后端与挂载数据形式。
- 后端是并排的:
ctx.storage.backend是一张名称到后端的表,多个后端可以同时挂载。 - 路由不在枢纽:哪个后端服务哪个消费方,由消费方自己的配置决定(领域层的路由表)。
- 注册返回 disposer:
register(name, backend)重复名称或查找未知名称都会抛StorageError。 - dispose 只注销名称:后端由拥有它的插件在注销之后自行关闭。
- 生命周期键:每个后端插件会发布
storageBackendServiceKey(name),数据形式注入它,避免激活与注册竞态。 - 数据形式可挂载:
mount(form, facility)是一个 effect;同一键二次挂载抛duplicate-mount,未挂载就解析会抛form-not-mounted——组合方要按这个顺序安排插件。
DSH plugin 的后端约定:StorageBackend、KvFacet 与 KvUnit
一句话结论:一个后端拥有一个介质、提供可选操作组;unit 内每次调用原子且持久,但并发写入的先后由调用方负责。
interface StorageBackend {
readonly kv?: KvFacet
close(): Promise<void> // 幂等,所有在途写入排空后释放介质
}
- 可选 facet:后端不能提供某类数据就不实现该 facet,解析时会明确失败而不是静默降级。
- unit 描述符:
KvFacet.open(descriptor)打开具名 unit,描述符含名称、当前格式版本、可选兼容版本、表名清单与是否有全局单例槽。 - 命名约束:unit 名与表名必须匹配
UNIT_NAME_RE——既能当文件名也能当 SQL 标识符片段;记录键是任意字符串,绝不进入路径。 - 原子性边界:unit 不对并发写入做串行化,顺序由调用方负责;但每次单独调用在介质上是原子的,resolve 后即已持久。
- 错误语义:
single布局版本不同拒绝version-mismatch;per-record文档版本不在接受集合内读作不存在;介质无法解析拒绝malformed-medium。 - 两种现成后端:json 为每个 unit 整文件原子重发人类可读文件;sqlite 每行一份文档,适合频繁更新。
DSH plugin 的领域声明:DomainSpec、layout 与 invalidRecords
一句话结论:领域由拥有包声明一次,schema 用 zod 写;声明错误在模块加载时就抛,不等碰介质。
- 基本信息:
name必须匹配UNIT_NAME_RE,同时作为后端 unit 名;version是非负整数。 - 布局二选一:
single(默认,整个 unit 一份文档)或per-record(每条记录一份文档)。 - 兼容版本:
compatibleVersions列出当前 schema 仍能接受的旧版本;per-record 后端会读带这些版本戳的文档,并接受一份旧版整 unit 文件做一次性引导。 - 坏记录策略:默认整次 open 以
invalid-record拒绝——适合权威数据;可丢弃的派生数据可用'backup-and-skip',把坏记录移开并继续。 - global 槽:可选单例;schema 不接受
null,因为null是介质的「从未写入」哨兵。 - 声明工具:
defineDomain(spec)固定字面量类型并在加载时校验;domainTable(schema)声明表;descriptorOf(spec)投影出后端描述符。
DSH plugin 的打开与读写:写链与 domain/changed
一句话结论:读是同步内存态,写走逐领域写链——先落盘、再更新内存、最后发事件;失败时内存不动。
- 打开:
ctx.storageDomain.open(spec)按严格顺序执行,任一步失败整体失败(already-open、backend-not-found、facet-unsupported、version-mismatch、malformed-medium、invalid-record)。 - 句柄稳定:
domain.table(name)重复调用返回同一实例;global 在首次 set 之前返回 spec 的initial。 - 读取:
get/entries/keys/size来自权威内存态,快照迭代器在排队写入落地期间保持稳定。 - 写入:
put、delete、update、global.set都在同一条逐领域写链上排队;后端拒绝时内存原样不动,读取绝不偏离介质。 - 更新语义:
update(key, fn)是写链 slot 上的原子读—改—写,键缺失拒绝missing-key;delete不存在的键返回 false,不写不发事件。 - 事件:每次持久写入在后端确认后发
domain/changed;put携带新快照(不含旧值),deleted是不带值的墓碑;事件仅限进程内。
DSH plugin 的路由与生命周期:backend、routes 与 close
一句话结论:路由是领域插件的配置,句柄由调用方关闭,facility 只兜底卸载时仍开着的领域。
- 路由配置:
backend是必填默认路由,routes按领域名逐个覆盖;枢纽不参与选择。 - 谁打开谁关闭:调用方拥有返回的句柄,用
Domain.close()释放——通常挂成自己的ctx.effectdisposer。 - 关闭语义:立即拒新写、排空已排队写入(事件仍会发)、释放 unit,然后释放名称供后续重新打开;幂等。
- 兜底:插件卸载时仍打开的领域由 facility 关闭;
closeAll()是卸载路径。 - 诊断:
get(name)是无类型的诊断查找,不要拿它替代类型化句柄。 - 验证:写入后立刻读应看到新值;后端拒绝时读应保持旧值——这两条能确认写链没有倒置。
DSH plugin 排查注意事项
一句话结论:存储问题先看「哪一层拒绝」,再区分路由、schema 与并发顺序。
backend-not-found/facet-unsupported→ 查领域路由配置与后端是否实现 kv facet。invalid-record→ 查 schema 与已存数据;派生数据可考虑backup-and-skip。missing-key→update前确认键存在,或改用put。- 内存与介质不一致 → 检查是否就地修改了返回对象,必须用
put/update替换。 - 并发写顺序乱 → unit 不串行化并发写,顺序由调用方保证。
- 修复状态如实:本文的枢纽/后端/数据形式分工、DomainSpec 字段、写链顺序与 domain/changed 语义均取自官方「存储」页面;接口签名以该页 Cordis API 与你所用版本为准。
常见问题
不是,DSH plugin 的存储子系统只管不属于会话事件日志的数据,会话日志有自己的持久化 seam。两者分开的好处是:会话日志保持仅追加语义,而其它数据可以用 KV 或 SQL 后端按需存储。
不会,DSH plugin 的存储枢纽只做汇合:后端拥有介质、数据形式拥有语义,枢纽自身不做任何 IO。产品包也不直接触碰后端,一律通过 ctx.storageDomain 的类型化 API。
DSH plugin 的 DomainSpec 里 single 把整个 unit 存成一份文档,per-record 让每条记录各自成文档。记录大、稀疏或可单独丢弃(例如投影缓存)时用 per-record,其余用 single;per-record 的版本校验也按记录进行。
DSH plugin 的 domain/changed 严格在后端确认持久之后发出,顺序遵循该领域的写链。它是通知而不是事务参与者:提交点已经过去,监听器同步抛错只会被兜住记录,不会让写入回滚。
不能,DSH plugin 返回的记录就是存储的对象本身而不是副本,就地修改会绕过写链。要改内容请通过 put 或 update 整体替换,否则内存态与介质会不一致。
相关术语
- StorageBackend(存储后端)
- StorageBackend 是 DSH plugin 存储里拥有一个介质的实现,提供可选的操作组与幂等的 close。它只负责把数据写进介质,不决定语义,也不被产品包直接调用。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/storage
- DomainSpec(领域声明)
- DomainSpec 是领域拥有包对自身身份、版本与记录布局的静态声明,含名称、版本、layout、兼容版本、可选的 global 槽与表定义。它是该领域 schema 的单一来源。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/storage
- KvUnit(存储单元)
- KvUnit 是后端打开的一个具名 unit,提供 loadAll、putRecord、deleteRecord、setGlobal 与 close。unit 名与表名必须匹配 UNIT_NAME_RE,而记录键可以是任意字符串、绝不进入文件路径。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/storage
- domain/changed
- domain/changed 是 DSH plugin 存储的变更事件,在后端确认持久之后按写链顺序发出。put 携带新快照而不携带旧值,deleted 是不带值的墓碑,事件仅限进程内。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/storage
来源
- DeepSeek Harness 文档 — 存储(枢纽、后端约定、DomainSpec、打开领域、domain/changed)· deepseek-harness.github.io
- dsh-storage 后端约定源码(backend.ts)· deepseek-ai(GitHub)
- dsh-storage-json 与 dsh-storage-sqlite README(两种后端取舍)· deepseek-ai(GitHub)