DeepSeek Harness 源码解析 · 第 31 讲
DeepSeek Harness 源码解析系列 第 31 讲:Storage 系统与 Settings 配置 基于 DeepSeek Harness 源码 · 2026-09-14 💡 本讲一句话: Harness 把"存东西"拆成三层——存储枢纽(只管路由、不做 IO)、域层(schema 校验 + 每域一条写链,先落盘后改内存)、设置层(用户可编辑的 YAML/JSON 文档,热重载 + 乐观并发 + 密钥脱敏)。读完你能看懂任何一次 settings.yaml 修改是怎么安全落到磁盘、又怎么被另一个进程看到的。 一、三层存储:谁负责什么 前几讲我们跟着 session 事件日志走完了 JSONL/SQLite 两条持久化路径。这一讲往上退一步,看支撑它们的 通用存储底座 :packages/storage(枢纽 + 后端契约)、packages/storage-domain(域层运行时)、packages/settings(用户设置能力)。三者的分工可以用一句话概括—— 枢纽不做 IO、后端只管介质、域层管语义、设置层管用户体验 。 先看全景对照,后面每一节都会回到这张表里对应的行: 层 包 职责 是否做 IO 存储枢纽 storage 命名后端注册表 + 数据形态挂载点(ctx.storage) 否,纯路由 后端契约 storage-json / storage-sqlite 拥有一个介质(文件树/数据库),提供 kv facet:loadAll / putRecord / deleteRecord / setGlobal 是,原子 + 持久化 域层 storage-domain spec 声明 + zod 校验 + 每域一条写链 + domain/changed 事件(ctx.storage.domain) 否,委托后端 设置层 settings / settings-file 命名空间注册、三层解析(defaults→base→user)、热重载、乐观并发、密钥脱敏(ctx.settings) 是,单文档读写 注意一个容易混淆的点:session 持久化(第 29/30 讲)走的是自己的 JSONL/SQLite 后端, 不经过 本讲的域层;而 todo、goal、plan 这类"结构化小状态"才用 storage-domain。设置层则是完全独立的一条线——它直接读写一个 YAML/JSON 文档,跟存储枢纽没有依赖关系。两条线在"原子写 + 跨进程锁"上共享同一套底层工具(dsh-atomic-write)。 二、存储枢纽:一个不做 IO 的注册表 packages/storage/src/index.ts 里的 Storage 服务只有两件事: 按名字管理后端 、 挂载数据形态(data form) 。它自己一个字节都不读写。 📄 packages/storage/src/index.ts (第 64-93 行) mount(form, facility) { // 把一种"数据形态"(如 domain)挂到枢纽上;挂载是 effect,返回 disposer if (this.forms.has(form)) { // 同名形态重复挂载直接抛错——fail loud,不静默覆盖 throw new StorageError('duplicate-mount', `storage form '${String(form)}' is already mounted`) } this.forms.set(form, facility) // 记入 forms 表,之后 ctx.storage.<form> 就能取到它 return () => { // 返回 disposer:卸载时只移除"还是自己"的那条记录 if (this.forms.get(form) === facility) { // stale-disposer 守卫:dispose 后重新挂载过,旧 disposer 再触发也不能误删后继者 this.forms.delete(form) } } } // 形态是可选成员——后端提供不了某种数据就干脆不挂,解析时 fail loud 为什么这样设计: "stale-disposer 守卫"( this.f…