第 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)是"该严格时绝不含糊,该放时诚实降级"- 任何由插件提供的注入内容,都要先问"它能不能跳出上下文"
本章目标
- 看清数据层七个组的真实规模:
client66 包最大,session21 包最厚 - 掌握 Typert 的四条生成期禁令——特别是"宁可构建失败也不弱化源类型"
- 看懂
window.__DSH_BOOT__为什么要转义< - 发现 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 项):
generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly.unsupported Zod projections fail with aTypertEmitErrornaming the construct instead of flattening or weakening the source type.Generation runs only at build time and never in a live agent session.The generated declaration file exposesTYPERTasunknown, 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 · 勘误总表。