DeepSeek Harness 源码精读 09:数据层与前馈端

第 09 章 数据层与前馈端

代码基线:commit 8f86b979a1(2026-10-01)| 0.2.0-rc.1 | 本系列讲次:M8

本章导读

这一章讲真相存在哪里、怎么被读出来、怎么被呈现。

七组包:client 66 个最大,session 21 个最厚。主角是三条线——持久化与非会话存储、投影与检索、Typert 与前馈端。

这一章书稿也几乎零勘误,所以重点在"补上书稿没写的视角"。最大的补充是 Typert 的四条生成期禁令,其中第 2 条值得逐字读:生成器遇到表达不了的 Zod 投影时,选择抛 TypertEmitError 并点名是哪个构造,而不是压平或弱化源类型。退化成 any 的代价会推迟到运行时、以错误形状的数据出现在对端;构建失败会立刻指出问题在哪。

另一条是 docs/capability-seams.md——一张由 scripts/gen-doc-graphs.ts 生成的 94 行接缝权威表,每行给出 ctx 键、类别、Definition、Provider、Consumer,带完整性守卫。这是全书最该被读者知道的一张表,第 10 章会用到它。

还有一个细节:启动注入 window.__DSH_BOOT__ 会把 < 转义成 <,因为注入行来自插件、内容可由第三方控制。

学习要点

  • ctx.storage 是命名后端注册表,同一值可挂多个后端
  • Typert 只在构建期运行,绝不在活的会话里
  • 生成器遇到搞不定的类型要报错,不要降级——这是所有生成器的正确默认值
  • 双 codec(strict / src-json)是"该严格时绝不含糊,该放时诚实降级"
  • 任何由插件提供的注入内容,都要先问"它能不能跳出上下文"

本章目标

  1. 看清数据层七个组的真实规模:client 66 包最大,session 21 包最厚
  2. 掌握 Typert 的四条生成期禁令——特别是"宁可构建失败也不弱化源类型"
  3. 看懂 window.__DSH_BOOT__ 为什么要转义 <
  4. 发现 docs/capability-seams.md 这个机器维护的权威表——它是 M6 手工体检表的官方版本

一、七个组的实测规模

组 包数 装什么
client 66 浏览器半区,ui-* 插件几乎全在这
session 21 持久化、投影、遥测、标题、格式迁移链(v0→v1→v2→v3→v4)
api 10 远程 BFF 控制器 + Typert 网关
host 10 Web 服务器、静态注入、插件清单
storage 4 中枢 + domain + 2 后端
session-query 4 检索、SQLite 索引、导出、模型可见工具
typert 4 协议 / 生成器 / 加载器 / 注册表

M3 的伏笔在这里闭合:那 5 个 session-format-vN-to-vM 包,正是本机 34 个会话出现 v2/v3/v4 多代并存的原因。


二、ctx.storage:与 ctx.shell / ctx.subagents 同构的注册表

四个包的实测 description:

包 定位
dsh-storage Storage hub (ctx.storage): named backend registry plus mounted data-form facilities
dsh-storage-domain Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains
dsh-storage-json JSON file KV storage backend
dsh-storage-sqlite SQLite storage backend(kv facet)

和 M6 的 shell 接缝一模一样的形状:定义 + 命名后端注册表 + 两个 Provider(文件 / SQLite)。书稿 §8.3 说它是"存储非会话的数据",成立,但没说它同样是"命名后端注册表"——也就是同一个值可以有多个后端。

storage-domain 那层特别值得注意:schema 校验 + 事件发射。这让它不只是 KV,而是"领域数据形态"——写入要过 schema,变更要发事件(M5 讲的那套 typed events)。


三、★ Typert:类型即契约,以及它的四条生成期禁令

四个包的分工(脚本第 3 项实测):

包 职责
typert-protocol Compiler-independent Remote metadata and Typert provider protocols
typert-generator TypeScript project analyzer and model-driven artifact generator
typert-loader Loader integration for generated package contributions
typert-registry Runtime registry for generated package reflection and Zod schemas

它是构建期工具,不是运行时组件。 README 抽取的四条原文(脚本第 3 项):

  1. generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly.
  2. unsupported Zod projections fail with a TypertEmitError naming the construct instead of flattening or weakening the source type.
  3. Generation runs only at build time and never in a live agent session.
  4. The generated declaration file exposes TYPERTasunknown, so contributing packages never depend on the runtime registry.

3.1 第 2 条是本章最有价值的一句

命名出错处的构造,而不是把源类型压平或弱化。

这是"fail loud"在代码生成场景下的形态。 想象一下另一种设计:生成器遇到搞不定的类型,就退化成 any 或 unknown——编译能过,运行时才发现 Client 拿到了错形状的数据。dsh 选择让构建失败,并且错误信息直接指出是哪个构造出的问题。

这和你那份 settings.yaml 一个空格炸全部模型路由 是同一条世界线的两端:配置写错要立刻炸;类型表达不了也要立刻炸。

3.2 第 4 条解决了循环依赖

生成的声明文件把 TYPERT 暴露成 unknown,所以贡献包不依赖运行时注册表。没有环:包 → 生成物(unknown)→ 注册表;包不直接依赖注册表。

3.3 双面编译

README 里:"Static consumers call WorkspaceAnalyzer directly against the workspace's tsconfig.host.json and tsconfig.client.json aggregates, select a face and package subset"。

这正是 AGENTS.md 说的"Host / Client 双隔离"的落地——M9 会展开。


四、★ 两种 codec:strict 与 src-json

书稿 §8.6.1 讲的双轨,实测确认(typert/protocol/src/types.ts:291):

模式 语义
strict 生成的精确 schema,运行时按它校验,类型不对就大声失败
src-json 只保证 JSON 安全("enforce JSON-safe values"),不做结构类型恢复

书稿那句判断一字不差:

Typert 不假装自己有精确类型,而是诚实地退到"JSON 安全"这一档……该严格时绝不含糊、该放时诚实降级。

而 §8.6 末尾关于"resolver 卸载后仍保留 wire 声明"的那段("一旦'这是 Host 对象引用'这个类型声明写下,即便实现的提供方走了,这个参数的'身份语义'也绝不静默漂移")——把类型当成比实现更持久的承诺,这是本章的第二条深刻观察。


五、★ window.__DSH_BOOT__:连"转义 <"都写进了书稿

实测 packages/host/webserver/src/injections.ts:47-55:

// `<` is escaped in JSON so a row-controlled string cannot break out of ...  
const name = JSON.stringify(row.name).replaceAll('<', '\\u003c')  
const value = row.value === undefined ? ... : JSON.stringify(row.value).replaceAll('<', '\\u003c')  

对应测试断言(webserver.spec.ts):

'globalThis["__DSH_BOOT__"] = {"rev":"\\u003c/script>\\u003cb>"}'  

注入源是 webserver/index-inject 事件,由插件推送(第三方可控),所以一个字符串就能闭合 <script> 标签。转义 < 是必须的。

书稿 §8.7.1 结尾那句"并且把 < 转义,防止任何受插件控制的字符串能跳出这个 script 元素"——完全属实,一个字不差。


六、★ 本章最大的收获:docs/capability-seams.md 是机器维护的接缝权威表

脚本第 5 项实测:

表内接缝行数: 94  
生成说明: <!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.  
含双语文档: 是(中英同源,门禁校验行对齐)  

94 行接缝,每行给出 ctx 键 / 类别 / Definition / Provider / Consumer。

这直接回答了 M6 留下的问题:我在 M6 手写了一个体检脚本,因为当时不知道这张表已经存在。它就是那张表的机器版本,而且带完整性守卫(packages/AGENTS.md:"interface/implementation/consumer roles are classified in scripts/gen-doc-graphs.ts with a completeness guard")。

文档末尾还给出了接缝类别的枚举,从抽取的行里能读到这几种:

类别 例
core ctx.clientModules、ctx.dynamicCordisRunner、ctx.cordisInspect
seam ctx.workflowEngine、ctx.lsp

行动项:M6 的 M6-seams-lab.ts 可以退役或改造成"对着这张生成表做交叉验证"。我在 M9 会处理这件事。



本章实验(附录)

七、动手验证

export PATH="/opt/homebrew/bin:$PATH"  
cd /Users/ygs/ygs/deepseek-harness  
node --import tsx/esm "ygsdoc/学习笔记/labs/M8-data-lab.ts"  

五组输出:① 七个组的包数;② storage 四包定位;③ Typert 四包分工 + 四条生成期禁令原文;④ boot 注入的转义实现 + 注释 + 测试断言;⑤ capability-seams.md 的行数、生成说明与双语状态。

M8 通过标准:能说出 Typert 遇到无法表达的 Zod 投影时为什么选择让构建失败而不是退化成 any——因为退化的代价会推迟到运行时、并且以错误形状的数据出现在 Client 端;而构建失败会立刻指出是哪个构造出的问题(第 2 条禁令)。


实验脚本:labs/M8-data-lab.ts

运行(在仓库根目录):

export PATH="/opt/homebrew/bin:$PATH"  
cd /Users/ygs/ygs/deepseek-harness  
node --import tsx/esm "dsh-源码精读/labs/M8-data-lab.ts"  

预期输出(本机实测,任何一行对不上就说明基线变了):

1) 数据层与前馈端:七个组的包数  
──────────────────────────────────────────────────────────────────────  
2) ctx.storage 与 ctx.storage.domain(1 定义 + 2 Provider + 1 横切)  
──────────────────────────────────────────────────────────────────────  
3) Typert:把 TypeScript 类型图变成两端的 codec  
──────────────────────────────────────────────────────────────────────  
    generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly.  
    unsupported Zod projections fail with a `TypertEmitError` naming the construct instead of flattening or weakening the source type.  
    Generation runs only at build time and never in a live agent session.  
    exposes `TYPERT` as `unknown`, so contributing packages never depend on the runtime registry.  
4) window.__DSH_BOOT__ 的注入与转义  
──────────────────────────────────────────────────────────────────────  
   转义实现: JSON.stringify(row.value).replaceAll('<', '\\u003c')  
   代码注释: // `<` is escaped in JSON so a row-controlled string cannot break out of  
   测试断言: 'globalThis["__DSH_BOOT__"] = {"rev":"\\u003c/script>\\u003cb>"}',  
   → 注入行来自插件(webserver/index-inject 事件),内容可由第三方控制,  
     所以 `<` 必须转义,否则一个字符串就能闭合 <script> 标签  
5) docs/capability-seams.md —— 生成的接缝权威表  
──────────────────────────────────────────────────────────────────────  
   表内接缝行数: 94  
   生成说明: <!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.  
   含双语文档: 是(中英同源,门禁校验行对齐)  
   → 这张表就是 M6 手工做的"接缝三件套体检表"的机器版本,且带完整性守卫  
   …(完整输出见运行脚本)  

本章的勘误条目见 附录 A · 勘误总表。