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 侧启动 · 就绪发布。
架构
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 发布。
启动与连接的实际流程
- 壳侧的
start()保证同一DesktopHostProcess实例不会重复 spawn:若child已存在,直接返回同一个readyPromise。首次调用拼出 runtime 内的 Host 入口,把runtimeDir、projectDir、可选的 primary runtime 与包管理器路径作为位置参数传入;以项目目录为工作目录,stdio 包含ipc,stdout 转发至父进程 stdout,stderr 只保留末尾 64 Ki 字符供错误报告使用。源码。 - Host 从参数读取目录,安装 Office 引擎解析,基于安装锚点加载 profile,报告跳过的 bundles,使用
loadLayeredEnv('dsh')与profile: 'desktop'运行runProfile。patchFiles是空数组,args为['--no-open', '--port', '0'];这些是这条桌面启动路径实际传入的参数,不是所有 profile 的全局默认值。源码。 application完成后,Host 安装更新任务控制与退出检查,将desktopOffice注册到ctx,然后安装平台会话发布器。最后读取 Web 服务实际端口,并发送ready。壳侧仅当消息满足事件校验时才将{ url, injections }交付给等待启动的调用者。Host · 壳。
Sources: host-process.ts, index.ts, index.ts
可复用的实际代码片段
壳启动一次子进程并等待 Host 的连接事实;下面摘录的是实际入口定位与 spawn 配置:
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:
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 分派 · 截止时间声明。
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。
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
参数与配置
下表仅列出这条启动路径实际可见的配置和常量;不能由此推断整个产品的配置默认值。
| 入口 / 选项 | 类型或值 | 此路径中的取值与用途 |
|---|---|---|
runtimeDir | string | 壳传入的不可变运行时包目录;用于定位 Host 的 lib/index.js,Host 侧用于定位安装锚点。壳 · Host |
projectDir | string | 插件 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_CHARS | 64 * 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 · 壳。
相关链接
- 开发与构建顺序:Host 构建阶段与桌面壳单独打包的边界。
- Desktop Host 启动入口:profile、控制通道与 Web URL 发布。
- 壳侧进程管理:启动、IPC 校验和就绪事件。