DeepSeek Harness 源码解析 · 第 37 讲
DeepSeek Harness 源码解析系列 第 37 讲:Web 客户端架构 基于 DeepSeek Harness 源码 · 2026-09-20 💡 本讲一句话: DeepSeek Harness 的 Web 客户端不是"一个 React 应用",而是一张 插件图 ——壳内核(boot kernel)只干三件事:加载页、模块表播种、失败时大声报错;所有业务 UI 都是插件通过 slot 注册拼出来的。读完你能看懂两阶段启动为什么必须等整个 immediately 层、HTTP RPC 和 WebSocket 下行流如何分工、以及"组件永远看不到 ctx"这条红线是怎么落地的。 一、入口只有 10 行:壳内核才是主角 前几讲我们一直在看宿主侧(host)的 runtime。这一讲切到浏览器侧。先看最反直觉的一点: apps/web 这个"应用"本身只有 10 行代码 ——它只负责找到挂载点,然后把一切交给 @deepseek-ai/dsh-client-web : 📄 apps/web/src/main.ts (第 6-10 行) import { AppWebEntry } from '@deepseek-ai/dsh-client-web' // 唯一依赖:壳内核入口类,加载/组装/插件装配全在它里面 const el = document.getElementById('root') // 找挂载点——这是整个"应用"里唯一的 DOM 操作 if (el === null) throw new Error('web app: missing #root') // 找不到直接抛错:fail loud,不渲染任何兜底 UI void new AppWebEntry(el).run() // 把挂载点交给内核跑两阶段启动;void 表示不 await——失败由加载页自己呈现 为什么入口要这么薄?因为壳内核有一条 自足性规则(shell self-sufficiency rule) :加载页必须在"插件全部失败"时也能工作——它呈现的恰恰是"哪些插件失败了"。如果加载页本身依赖某个插件,那插件挂了你就连错误报告都看不到。所以 boot.tsx 的文件头注释把这条规则写成了硬约束: 🔹 壳自足性规则: "这里的一切都不能自己是 loader entry,也都不 value-import 任何插件包——加载页必须能在(尤其是当)插件失败时工作。唯一被允许的例外是 modules 包:模块系统不能通过自己到达自己,所以它的类被壳打包进内核,等 cordis 起来后再收养它的插件入口。" 整个浏览器侧的包结构也印证了这个分层: packages/client/ 下有 40+ 个 dsh-client-* 包,按职责分成三层—— runtime (数据对象层,零 React)、 web-react (渲染机制胶水)、 ui-* (业务 UI 插件)。壳内核(web)只认识前两层。 层 包 职责与红线 壳内核(shell) dsh-client-web、apps/web 两阶段启动、加载页、模块表播种;零插件依赖,失败时大声报错 数据对象层(React-free) dsh-client-runtime、dsh-client-connection ConnectionController → SessionManager → Session 拥有全部业务状态;grep 可断言零 React import 渲染机制(shell-only glue) dsh-client-web-react、dsh-client-ui-slots slot 渲染器/outlet、uSES 桥接;所有 ctx→React 的集成都在这层 业务 UI 插件 ui-layout、ui-conversation、ui-sidebar…(30+) 只通过 ctx.slots.register 组合 UI;组件永远看不到 ctx 这个分层不是审美偏好,而是可执行的纪律:仓库的 AGENTS.md 明确写着数据对象层"零 React import——grep-assertable"。下一节看壳内核怎么把这张图拉起来。 二、两阶段启动:模块面 → 插件面 AppWebEntry.…