Repository Wiki
deepseek-ai/deepseek-harness

Electron 壳、Desktop Host 与 Web 客户端连接

桌面端把 Electron 壳与运行 Web 应用的独立 Node 模式 Host 进程分开:壳启动 Host,Host 启动 desktop profile、取得本地 Web 服务的认证 URL,再通过进程 IPC 把连接信息交还给壳。

目的与范围

本文聚焦壳与 Desktop Host 的进程边界、Host 对 Web 服务的启动与连接信息交付,以及双方可见的 IPC 控制协议。Electron 窗口的具体导航、渲染进程实现、账号页面、更新 UI、Office 引擎与任务的内部实现属于相邻主题;本文只解释它们在这条边界上可见的消息或安装点。这里读取的源码没有展示 BrowserWindow 如何消费 ready.url,因此不能把窗口加载方式当作已验证事实。构建阶段的背景可另见 开发文档。

概述

DesktopHostProcess 是壳侧的子进程管理对象,使用 Electron 可执行文件的 Node 模式启动 @deepseek-ai/dsh-desktop-host;Host 侧入口 main() 加载 profile,调用 runProfile,并在服务可用后计算 ctx.connection.authenticatedUrl(...),将 URL 和 ctx.webServer.collectIndexInjections() 作为 ready 事件发送。由此,桌面复用 Web 应用,同时通过不同进程隔离壳的生命周期控制与应用服务启动。壳侧启动 · Host 侧启动 · 就绪发布。

架构

Loading diagram...

Sources: host-process.ts, index.ts, index.ts

这里 runProfile 的结果提供 ctx;Host 从 ctx.webServer.port 形成回环地址,再经 ctx.connection 生成认证 URL。ready 是 IPC 消息,不是 shell 自己猜测 Web 监听端口;监听端口由启动参数 --port 0 交给应用选择。Host 入口 · URL 发布。

启动与连接的实际流程

  1. 壳侧的 start() 保证同一 DesktopHostProcess 实例不会重复 spawn:若 child 已存在,直接返回同一个 readyPromise。首次调用拼出 runtime 内的 Host 入口,把 runtimeDir、projectDir、可选的 primary runtime 与包管理器路径作为位置参数传入;以项目目录为工作目录,stdio 包含 ipc,stdout 转发至父进程 stdout,stderr 只保留末尾 64 Ki 字符供错误报告使用。源码。
  2. Host 从参数读取目录,安装 Office 引擎解析,基于安装锚点加载 profile,报告跳过的 bundles,使用 loadLayeredEnv('dsh') 与 profile: 'desktop' 运行 runProfile。patchFiles 是空数组,args 为 ['--no-open', '--port', '0'];这些是这条桌面启动路径实际传入的参数,不是所有 profile 的全局默认值。源码。
  3. application 完成后,Host 安装更新任务控制与退出检查,将 desktopOffice 注册到 ctx,然后安装平台会话发布器。最后读取 Web 服务实际端口,并发送 ready。壳侧仅当消息满足事件校验时才将 { url, injections } 交付给等待启动的调用者。Host · 壳。
Loading diagram...

Sources: host-process.ts, index.ts, index.ts

可复用的实际代码片段

壳启动一次子进程并等待 Host 的连接事实;下面摘录的是实际入口定位与 spawn 配置:

typescript
1 const entry = join(this.runtimeDir, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'index.js') 2 const child = spawn(this.node, [ 3 '--expose-internals', 4 ...(this.inspectPort === undefined ? [] : [`--inspect=127.0.0.1:${String(this.inspectPort)}`]), 5 entry, 6 this.runtimeDir, 7 this.projectDir, 8 this.primaryRuntime ?? join(this.runtimeDir, '..', 'runtime', 'primary-runtime'), 9 ...this.packageManager === undefined ? [] : [this.packageManager.pnpm, this.packageManager.nodeBin], 10 ], { 11 cwd: this.projectDir, 12 env: desktopNodeEnvironment(this.node, undefined, this.environment), 13 stdio: ['ignore', 'pipe', 'pipe', 'ipc'], 14 })

Source: host-process.ts

Host 侧直接从 Web 服务和连接组件获取要传给壳的事实,而不是在父进程重构认证 URL:

typescript
const url = ctx.connection.authenticatedUrl(`http://127.0.0.1:${String(ctx.webServer.port)}`) if (process.connected) process.send?.({ type: 'ready', url, injections: ctx.webServer.collectIndexInjections() }, (error) => { if (error !== null) console.error(error) })

Source: index.ts

这两个例子是源文件中的实现摘录,不是独立可运行的教程脚本;其中 this 和 ctx 分别来自所在类与启动结果。

IPC 控制、关闭与错误边界

DesktopHostEvent 明确列出 ready、fatal、platform-session、shutdown-complete、update-tasks、quit-inspection 六类回传事件;后两类带 requestId,壳用 controlRequests 映射找到对应请求。收到带 error 的控制回执时 reject,否则 resolve;无对应 ID 时可选调用不会执行。shutdown-complete 仅在壳已进入 stopping 状态时被接受,意外确认关闭会被标记为失败。事件与映射 · 处理 · 消息分派。

Host 收到 shutdown 后只创建一次 stopping promise:等待已启动应用,调用 running?.shutdown.shutdown(0),回传 shutdown-complete 后断开 IPC;父进程断连也触发 stop()。如果启动本身失败,关闭路径不会试图关闭未成功启动的运行树。源码 · 断连。

控制消息在 Host 侧按请求 ID 回传:quit-inspection 在停止中或控制器尚未安装时返回带 error 的保守结果(activeTasks: true、scheduledTasks: false);update-tasks 只接受 inspect、lock、unlock,异常时以 active: true 加 error 响应。quit-inspection 的错误结果意味着壳不能将未知工作视作安全退出;壳侧另声明检查截止时间为 2,000 ms,超时视为未知工作并在退出前询问。Host 分派 · 截止时间声明。

typescript
1 const stop = (): Promise<void> => stopping ??= (async () => { 2 // Startup failure is reported by main; shutdown only owns a tree that booted. 3 const running = await application.catch(() => undefined) 4 await running?.shutdown.shutdown(0) 5 await send({ type: 'shutdown-complete' }) 6 if (process.connected) process.disconnect() 7 })()

Source: index.ts

校验与诊断

壳不信任任意 IPC 负载。isDesktopHostEvent 校验类型及必需字段;platform-session 要求非空 token,userId 为 null 或非空字符串,origin 必须是无凭据的 HTTPS 来源或 HTTP 回环来源;可选 requestHeaders 的键必须小写,值不能包含换行,且不允许覆盖授权、Host、长度、传输和内容类型等受限头。无效事件会导致壳记录失败并对 child 发 SIGTERM。校验 · 处理。

Host 启动异常由顶层 catch 转成 fatal:发送用户可见的 message 和经 inspect(error, { depth: 4, maxArrayLength: 50 }) 取得并截断至 64 Ki 字符的 diagnostic;还写 stderr、设退出码 1、断开 IPC。壳保留单独的 DesktopHostFatalError.diagnostic getter,避免把完整诊断作为普通错误属性重复转义输出。壳也保留 stderr 尾部 64 Ki 字符,并在 child 关闭时以退出码及 stderr 构造错误;具体错误处理的后续 UI 表现不在已读实现内。Host fatal · 壳错误类型 · child close。

typescript
1 if (!isDesktopHostEvent(message)) { 2 this.fail(new Error('dsh desktop host sent an invalid IPC event')) 3 child.kill('SIGTERM') 4 return 5 } 6 if (message.type === 'ready') this.readyResolve({ url: message.url, injections: message.injections }) 7 else if (message.type === 'platform-session') this.onPlatformSession?.(message.session)

Source: host-process.ts

参数与配置

下表仅列出这条启动路径实际可见的配置和常量;不能由此推断整个产品的配置默认值。

入口 / 选项类型或值此路径中的取值与用途
runtimeDirstring壳传入的不可变运行时包目录;用于定位 Host 的 lib/index.js,Host 侧用于定位安装锚点。壳 · Host
projectDirstring插件 profile 目录、child 工作目录;Host 以此加载 profile。壳 · Host
inspectPort可选 number未指定则不加 inspector 参数;指定后为 --inspect=127.0.0.1:<port>。源码
primaryRuntime可选 string未传时壳使用 join(runtimeDir, '..', 'runtime', 'primary-runtime');Host 的 desktopOffice 使用第四个位置参数,缺省采用同一相对路径。壳 · Host
packageManager可选 { pnpm: string; nodeBin: string }存在时壳传入额外位置参数;Host 构造以 Electron Node 可执行文件运行 pnpm 的 command/args/env,并把提供的 nodeBin 前置到 PATH。壳 · Host
profile、patchFiles、args'desktop'、[]、['--no-open', '--port', '0']Host 传给 runProfile 的固定值:不主动打开浏览器,Web 服务使用动态端口。源码
MAX_HOST_DIAGNOSTIC_CHARS / MAX_FATAL_DIAGNOSTIC_CHARS64 * 1024 字符分别限制壳收集的 stderr 尾部与 Host 通过 IPC 发送的启动错误诊断。壳 · Host

边界 API 与扩展位置

  • new DesktopHostProcess(node: string, runtimeDir: string, projectDir: string, inspectPort?: number, environment: NodeJS.ProcessEnv = process.env, onFailure?: (error: Error) => void, primaryRuntime?: string, packageManager?: { readonly pnpm: string; readonly nodeBin: string }, onPlatformSession?: (session: PlatformSession | null) => void):构造壳侧子进程管理对象;回调参数用于报告意外失败与私有平台会话更新。构造函数体为空,启动工作在 start() 执行。签名及参数说明。
  • start(): Promise<DesktopHostReady>:首次调用启动 child,后续调用复用 readiness promise;返回数据包含 url: string 与可选 injections?: readonly unknown[]。错误通过内部 fail 路径通知及拒绝 promise;已读代码未展示 fail 的完整实现,故不规定其所有拒绝条件。类型 · 签名与流程。
  • main(): Promise<void>:Host 模块内入口;由 import.meta.main 条件启动,不是供外部直接调用的导出 API。扩展 Host 启动行为时须注意当前顺序:完成 application、安装控制器与 office 插件、安装 session 发布器,然后发送 ready。源码 · 安装顺序 · 执行守卫。

运行与一致性注意事项

  • IPC 的请求-回执相关性依赖整数 requestId,壳侧校验为安全整数;Host 侧对未知消息、无效 ID 或未知更新动作直接忽略。不要将新消息类型仅加入发送端而不更新接收端校验与分派。壳校验 · Host 分派。
  • ready 消息可携带 injections,但当前壳侧校验只检查 url 是字符串;不能把此处当作 URL 目标或 injections 内容的完整验证。平台会话则有明确的 origin、token、header 校验。源码。
  • stop() 使用单一共享 promise,避免同时收到控制消息和断连时重复关闭应用;start() 使用同一 readiness promise,避免同一实例重复启动 child。这是实例内去重,不意味着多个 DesktopHostProcess 实例共用一个 Host。Host · 壳。

相关链接

Sources

(2 files)
apps/desktop-host/src
apps/desktop/src