Deepseek-harness增加桌面版端序列:第 4 讲 · 组合与装载:runProfile → boot()

第 4 讲 · 组合与装载:runProfile → boot()

系列 :我在Deepseek harness中增加了桌面版,将逐行代码解析我是怎么增加的,希望你也能创建属于自己的桌面版Agent 本讲目标 :逐段吃透从 runProfileboot() 的装载链路------四层 patch 如何合并、boot() 如何创建 Context 并装载整棵插件树、以及"config-only HMR"如何让 cordis.patch.yml 的编辑热生效。 逐行文件apps/cli/src/profile-boot.ts(301 行,关键段)+ packages/boot/app-boot/src/index.ts(boot 函数,约 46 行全行)
代码仓库github.com/tslcarmack/... Desktop App 运行效果图


🎯 本讲地图

scss 复制代码
runProfile({ profile: 'desktop', ... })         [profile-boot.ts]
  ├─► composeProfile('desktop', patches)
  │      ├─► prepareProfile()       → 修复模块回退 + 加载 profile + 重写空根
  │      ├─► bundlePatches          → dsh-base + dsh-desktop-app 的 patch 层
  │      ├─► profile.patches        → ~/.dsh/profiles/desktop/cordis.patch.yml
  │      ├─► homePatches            → ~/.dsh/cordis.patch.yml
  │      └─► overlays               → --patch + telemetry 开关
  ├─► boot(NAME, rootConfig, allPatches, prepare)   [app-boot/index.ts]
  │      ├─► new Context()
  │      ├─► ctx.plugin(Loader)     → 安装 Loader 插件
  │      ├─► prepare?.(ctx)         → 注入环境/命令行快照
  │      ├─► mountRootInclude()     → 装载插件树(核心!)
  │      ├─► loader.await()         → 等所有 entry 启动
  │      └─► assertEntriesActivated → 审计:有 entry 没启动就报错
  └─► watchUserPatches()            → config-only HMR(编辑即热生效)

📁 1. composeProfile:四层 patch 合并(profile-boot.ts 第 142-171 行)

ts 复制代码
142  function composeProfile(
143    name: string,
144    patchFiles: readonly string[],
145  ): ComposedProfile {
146    const profile = prepareProfile(name)
147    const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
148    const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
149    const bundlePatches = profile.layers.flatMap(layer => layer.patches)
150    const rows = new Map()
151    for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
152      if (typeof row.id === 'string') rows.set(row.id, row)
153    }
154    const composedOverlays = [...overlays]
...
168    const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
169    if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
170    return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
171  }

逐段讲解:

内容 说明
146 prepareProfile(name) 修复模块回退 + 加载 profile 元数据 + 重写空根 cordis.yml
147 homePatches 机器级 ~/.dsh/cordis.patch.yml(跨 profile 生效,优先级高于 profile 层
148 overlays --patch 参数指向的文件(可重复)
149 bundlePatches desktop = dsh-base, dsh-desktop-app 两个 bundle 的 patch 拼接
151 composeEntries([...]) 核心合并函数:按序应用四组 patch,同 id 后层胜出
152-153 rows 建 id → 行的索引,供后面的"telemetry 开关"检查该行是否存在
168-169 telemetry 开关 DSH_TELEMETRY_DISABLED 非空即禁用遥测行(隐私开关:宁错关不误开

1.1 prepareProfile(第 98-103 行)

ts 复制代码
export function prepareProfile(name: string, userLayer = true): Profile {
  healProfilesModuleFallback(INSTALL_ANCHOR)
  const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
  writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
  return profile
}
  • healProfilesModuleFallback修复 profile 目录下的符号链接 (你之前遇到过的 .dsh\profiles\node_modules 损坏问题就是这里处理的)。
  • loadProfile:读取 ~/.dsh/profiles/desktop/package.jsondsh.profile.bundles
  • 重写 cordis.yml 为空根 []------因为整棵树都是 patch 层,根必须保持空(防止 Loader 写回导致 bundle 行重复)。

📁 2. boot():装载整棵树(app-boot/index.ts 第 760-805 行,全行)

ts 复制代码
760  export async function boot(
761    binName: string,
762    absoluteConfigPath: string,
763    patches?: PatchOptions[],
764    prepare?: (ctx: Context) => Promise | void,
765    bareModuleBaseUrl?: string,
766  ): Promise {
767    const ctx = new Context()
768    let stage = 'host preparation failed'
769    try {
770      ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/'
771      ctx.provide('dshHomePath', dshHomePath)
772      await ctx.plugin(Loader)
773      await prepare?.(ctx)
774      stage = 'plugin tree failed to load'
775      await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl)
776      await ctx.get('loader')?.await()
777      if (ctx.get('loader') === undefined) return ctx
778      await assertEntriesActivated(ctx, binName)
779      return ctx
780    } catch (cause) {
781      await ctx.fiber.dispose()
782      const detail = cause instanceof Error ? cause.message : String(cause)
783      let deepest: unknown = cause
784      while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause
785      const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : ''
786      throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause })
787    }
788  }

逐行讲解:

内容 说明
767 new Context() 创建 Cordis 根上下文------一切插件的宿主
768 stage = 'host preparation failed' 失败标签:prepare 失败 = 宿主问题,不是插件树问题(两个阶段两种诊断)
770 ctx.baseUrl = ... 以 config 文件目录为基准,供相对路径解析
771 ctx.provide('dshHomePath', dshHomePath) 注入 dshHomePath 工具函数(供 !!js dshHomePath('storages') 这类表达式使用)
772 await ctx.plugin(Loader) 安装 Loader 插件------装载器的装载器
773 await prepare?.(ctx) 第 3 讲 main.ts 传入的 prepare:provideCmdline + env 快照(在任何插件挂载前,保证插件解析环境值时来源一致)
774 stage = 'plugin tree failed to load' 切换失败标签
775 mountRootInclude(...) 装载插件树(下一节详解)
776 loader.await() 等待所有 entry 启动/失败
777 二次检查 若树已被 dispose(surface 提前退出),直接返回
778 assertEntriesActivated 最终审计:有 entry 从未激活(pending/failed)就抛错,列出等待的服务名
780-786 失败处理 先 dispose 部分 Context(清理半棵树)→ 剥离最深 cause 的 stack 附到诊断上(保留真实失败点)

📁 3. mountRootInclude:装载的最后一公里

mountRootInclude 内部大致流程(函数在 index.ts 前文,这里讲机制):

scss 复制代码
mountRootInclude(ctx, configPath, patches)
  ├─► include 插件解析 cordis.yml(空根 [])
  ├─► 应用 patches(bundle 层 → profile 层 → home 层 → overlays)
  ├─► 每个 entry 按 id 插入/覆写
  └─► Loader 逐个 entry:解析插件包 → apply(ctx) → 挂载服务

核心机制(为什么能"一切皆插件"):

  • include(cordis-plugin-include)负责配置行的解析与合并。
  • Loader(cordis-plugin-loader)负责代码 的装载------按 entry 的 plugin 字段解析包,调用其 apply(ctx),所有注册都是 effect(卸载自动撤销)。
  • 两者是 Cordis 生态的"配置层 + 运行时层",dsh 在它们之上叠了 profile/patch 组合语义。

📁 4. config-only HMR(profile-boot.ts 第 268-298 行)

ts 复制代码
268    if (!signalShutdown.signal.aborted
269      && ctx.fiber.state === FiberState.ACTIVE
270      && ctx.get('loader') !== undefined) {
271      try {
279        if (ctx.get('hmr') === undefined) {
280          if (ctx.get('timer') === undefined) {
281            await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-timer' })
282          }
283          await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-hmr', config: { root: [] } })
284        }
285        await watchUserPatches(ctx, {
286          binName: NAME,
287          filename: composed.profile.patchPath,
288          compose: composeLive,
289        })
290        await watchUserPatches(ctx, {
291          binName: NAME,
292          filename: homePatchPath(),
293          compose: composeLive,
294        })
295      } catch (error) {
296        suppressShutdownError(ctx, signalShutdown.signal, error)
297      }
298    }

逐段讲解:

  • 第 268-270 行:三个前提------未收到退出信号、树还活着、Loader 在。
  • 第 279-284 行 :如果组合里没有 HMR 服务(desktop-app 明确禁用了共享的 hmr 行,见第 5 讲),就挂一个 watch-only 的 hmr 实例(root: [] 无模块根,只 watch 配置)。
  • 第 285-294 行 :监听两个用户 patch 文件(profile 层 + home 层)------编辑即热生效,不用重启(bundle 层不可被用户编辑,所以不 watch)。
  • composeLive(第 240-245 行):每次重组都 structuredClone 全量 patch------注释解释得很清楚:include 的 insert 是按引用推进树的,复用同一个解析对象会把用户覆盖烤进 bundle 默认行,导致撤销失效。

🖼️ 第 4 讲流程图

图 4-1 · 从 composeProfile 到 boot 的完整装载链


⚙️ 机制小结

  1. 四层 patch :bundle(base+desktop-app)→ profile 层 → home 层 → overlays,同 id 后层胜出,根永远是空 []
  2. boot() 三段式:创建 Context → 安装 Loader + prepare → mountRootInclude 装载 → 审计激活。两阶段失败标签让诊断精准。
  3. 可逆注册的落地 :所有插件通过 effect 注册,ctx.fiber.dispose() 一处调用即可拆掉整棵树(第 3 讲窗口关闭就是这么做的)。
  4. config-only HMR:只 watch 用户 patch 层,bundle 层不动;每次重组全量 clone 防止"覆盖烤进默认"。
  5. telemetry 隐私开关DSH_TELEMETRY_DISABLED 任何非空值都禁用(宁错关不误开)。

🧪 动手验证

sh 复制代码
# 1) 看 desktop 组合树的层级注释(每个 bundle 的标记行)
cd D:\code\deepseek-harness
node apps/cli/lib/bin.js --profile desktop --dump-config | grep "^# ==" 

# 2) 数一下 desktop 树总行数
node apps/cli/lib/bin.js --profile desktop --dump-config | wc -l

# 3) 验证 config-only HMR:改 ~/.dsh/profiles/desktop/cordis.patch.yml 加一行
#    然后看桌面窗口是否热生效(无需重启)

📚 深入指引

文件 作用
apps/cli/src/profile-boot.ts 组合 + boot + HMR(本讲关键段)
packages/boot/app-boot/src/index.ts boot / mountRootInclude / assertEntriesActivated
packages/boot/app-boot/src/profile.ts loadProfile:bundle 名 → patch 文件解析
vendor/cordis/ Loader / Include 的 vendored 源码

下一讲预告 :desktop-app bundle 如何"改造"dsh------387 行的 cordis.patch.yml 里藏着什么(desktopRuntime 服务从哪来、为什么禁用了 HMR、浏览器端插件怎么组织)。→ 第 5 讲:desktop-app bundle

相关推荐
dong_junshuai2 小时前
每天一个开源项目#73 Munder Difflin:2.3K Star 的本地多Agent办公室
开源·github·agent
leeyi2 小时前
Langfuse 集成源码:batch 协议、media 上传与 mock 测试(第89篇-E75)
llm·aigc·agent
修远客2 小时前
风格进化:让Agent越来越懂你 — 从"工具"到"助手"的关键跃迁
llm·agent
用户469368483202 小时前
Deepseek-harness增加桌面版端序列:第 3 讲 · Electron 主进程:这个进程就是 harness
agent
武子康2 小时前
Pi Extension 写完不等于可用:从类型检查到真实 Runtime 的证据阶梯
人工智能·llm·agent
苏灿烤鱼4 小时前
上下文写成文件系统,为什么向量还能静默丢?
python·github·agent
怕浪猫12 小时前
DeepSeek Harness 源码实战 第4章:Session 会话日志——单一事实源
agent·产品·资讯