桌面应用:Electron 的三层

ZCode 桌面端怎样把 Electron 拆成 Main、窗口级 Local Host 与 Renderer:Host 用 Electron 自带的 Node 拉起 app-server 模式的 Agent 子进程并管理其生命周期,任务列表怎样与 Agent 会话库同步;另有 V8 字节码试验、内嵌浏览器、自动更新、终端、Claude Code 历史导入与打包签名。

作者 David更新于 44 篇(共 47 篇)

ZCode 的桌面端是一个 Electron 41 应用(packages/desktop/package.json:76)。packages/desktop/src 的非测试代码约 6 万行:Electron 主进程 main 占 4.7 万行,其中内嵌浏览器 main/browserView 一处就有 1.1 万行;窗口宿主 host 约 1 万行;preloadrenderer 加起来不到 2600 行——界面来自共享的 @zcode/ui,Renderer 只是一层壳。业务服务在 packages/services,Agent 是独立的子进程,两者之间走 stdio 上的 ZCode Protocol。

这一篇讲这些进程怎样分工、谁拉起谁、状态归谁所有。协议本身见ZCode Protocol V4,远程工作区与手机远控留给远程工作区与手机远控

位置职责
packages/desktop/src/mainElectron 主进程:窗口、菜单与托盘、深链、自动更新、内嵌浏览器、跨窗口广播,拉起 Host 与 Scheduler
packages/desktop/src/host每个窗口一个的 utility process:装配 @zcode/services,经 MessagePort 暴露 RPC,拉起 Agent,持有远程连接
packages/desktop/src/preloadrendererwindow.zcode 桥与端口转交;接端口、建 RPC 客户端、挂上 @zcode/ui
packages/desktop/src/scheduler常驻的定时与闲时任务调度进程
packages/services/src/zcode-agentAgent 子进程管理、协议客户端、stdio 传输、任务索引同步
packages/services/src/zcode-sessiondesktop-continuous 链路上的会话操作

进程与分工

图表加载中…

AGENTS.md 给 Main 划的边界是(AGENTS.md:60):

Main 负责窗口、原生操作、进程调度和消息转发,不承载 task/session 业务状态。

照这个边界看各进程:

  • Mainapp.whenReady 里先读设置、应用自定义数据目录,等本地数据库准备好才拉起 Scheduler(packages/desktop/src/main/index.ts:1893),自动更新只在正式版身份下启用(main/index.ts:1945)。它并非完全无状态:TaskRealtimeBus 维护跨 Host 的运行租约、流镜像批次与回放缓冲(最多 60 批、512 KB,packages/desktop/src/main/taskRealtimeBus.ts:25),BroadcastHub 把一个 Host 发来的广播转给其他窗口的 Host(packages/desktop/src/main/broadcastHub.ts:12)。这些是路由与有界回放用的状态,任务与会话的权威数据不在这里。
  • preload。只暴露必须由 Main 参与的平台操作,注释特意说明凭据已迁到 Host 的 ICredentialServicepackages/desktop/src/preload/index.ts:240)。MessagePort 过 contextBridge 会被包成 Proxy、丢掉原生方法,所以改用 window.postMessage 的 transfer 原样交给页面(preload/index.ts:815)。
  • Renderer。收到端口后 connectViaMessagePortChannelClient,把 RemoteServiceAccess 交给 @zcode/uiRootpackages/desktop/src/renderer/src/main.tsx:303packages/client/src/messageport.ts:25)。组件只经 IPlatformService 做平台操作(AGENTS.md:52),接口注释说它只放“必须穿越进程边界且不适合做成 RPC service”的操作,文件、终端、凭据等业务服务走 RPC(packages/shared/src/platform.ts:518);桌面实现就是对 window.zcode 的一层包装(packages/desktop/src/renderer/src/desktopPlatform.ts:6)。
  • Scheduler。Main 以服务名 zcode-cron-scheduler fork 出来(packages/desktop/src/main/desktopCronScheduler.ts:61),每 20 秒轮询一次 tasks-index,错过超过 5 分钟的触发记为跳过(packages/desktop/src/scheduler/index.ts:33scheduler/index.ts:38);它只读写任务索引,到期任务经 Main 交给 Host 执行(scheduler/index.ts:1)。细节见定时任务与闲时任务

每个窗口一个 Local Host

Host 入口文件开头的注释把拓扑画得很直白(packages/desktop/src/host/index.ts:3):

/**
 * Host Process 入口 —— 每个窗口对应一个独立的 host process
 *
 * 同一窗口的 Renderer 和手机 都 attachment 到这个 Host:
 *   Renderer / Mobile ←MessagePort→ Window Host
 *                                      ├─ local services
 *                                      └─ remote connection registry
 *
 * 启动流程:
 * 1. main 进程通过 Electron `utilityProcess.fork()` 创建本进程
 * 2. main 进程只发送一次 init-local 初始化窗口 Host
 * 3. 后续远端 connect / scoped attachment 都由同一 Host 处理
 */

窗口的 dom-ready 触发时,Main 为它 fork 一个 Host,标签是 local- 加 webContents id(packages/desktop/src/main/desktopWindowLifecycle.ts:89desktopWindowLifecycle.ts:193)。随 InitLocal 消息一起发出的是一对 MessageChannelMainport2 给 Host,port1 投给 Renderer(packages/desktop/src/main/desktopHostProcess.ts:559)。

Host 的寿命属于窗口,不属于页面加载周期。Renderer 刷新时,Main 不杀旧 Host,而是给它补挂一条新端口,补挂失败才退回重建(desktopWindowLifecycle.ts:143):

    if (oldChild && oldChild.pid !== undefined) {
      try {
        const startupPayload = getDatabaseStartupPortPayload(oldChild);
        if (!startupPayload) throw new Error("Previous Host startup binding is unavailable");
        const { port1, port2 } = new MessageChannelMain();
        oldChild.postMessage(
          {
            type: HostMessageTypes.AttachServicePort,
            requestId: randomUUID(),
            attachmentId: randomUUID(),
            clientMode: "desktop-continuous",
            scope: { kind: "local" },
          },
          [port2],
        );
        win.webContents.postMessage(InternalChannels.ServicePort, startupPayload, [port1]);

上方注释交代了原因:旧实现 reload 时连 Host 带 CLI 一起杀掉,运行中的会话直接消失(desktopWindowLifecycle.ts:138)。AttachServicePort 也是手机与远程工作区复用的通道:Host 里每条 attachment 都新建一个 ChannelServer 和一个带独立 connectionIdclientMode 的连接作用域,但底下共用同一个 ServiceCollectionpackages/desktop/src/host/index.ts:1964host/index.ts:2073)。所以一个窗口里所有本地工作区共享一个 Host、一套服务、一个 Agent 进程管理器(AGENTS.md:61)。

数据库准备先于服务。收到 InitLocal 后,Host 不马上装配服务,而是先跑数据库准备:在 Worker 里迁移 tasks-index.sqlite,再对每个预热目录用同一个 CLI bundle 的存储入口迁移会话库(packages/desktop/src/host/hostDatabaseStartup.ts:46hostDatabaseStartup.ts:57)。后者也在 Worker 里运行,参数是 app-server --stdio --prepare-storage --cwd,30 秒内收不到第一帧状态就判超时(packages/desktop/src/host/storagePreparationProcesses.ts:130storagePreparationProcesses.ts:151)。其间 Renderer 显示数据库启动页(packages/desktop/src/renderer/src/main.tsx:356),早到的 attachment 先挂起,准备就绪后再接入,不会启动第二个执行者(host/index.ts:2702)。就绪后才调用 createLocalServices,身份是 runtimeSurface: "desktop_local_host"serviceAuthorityMode: "desktop-local"host/index.ts:2815),并为 Main 挑出的最近 3 个工作区预热 Agent(packages/desktop/src/main/startupWorkspace.ts:40host/index.ts:2881)。

关窗时 Main 给 Host 发 Dispose,至少等 3.5 秒才强杀,好让 Host 先回收 Agent 进程树;注释说若按 150 或 300 毫秒强杀,Host 先退出,app-server 子进程就可能被 init 接管成孤儿(desktopHostProcess.ts:644)。

拉起 Agent

Renderer 的调用经 MessagePort RPC 到达 Host 里的 zcodeAgentService,再由 ZCodeProtocolClient 写进 Agent 的 stdin;客户端可发的方法是旧版方法名与 v4/* 并存,注释说正在收敛到 v4(packages/services/src/zcode-agent/zcodeProtocolClient.ts:15)。Agent 的通知与事件沿原路回到 Renderer。

Agent 进程由 ZCodeAgentProcessManager 按工作区管理。命令来源有固定的先后(packages/services/src/zcode-agent/zcodeAgentProcessManager.ts:438):

export function resolveDefaultZCodeAgentCommand(
  context: ZCodeAgentCommandResolverContext,
): ZCodeAgentCommand | null {
  const command = process.env.ZCODE_AGENT_SERVER_COMMAND?.trim();
  if (command) {
    return applyPresentationSurfaceToCommand(
      {
        command,
        args: parseArgsJson(process.env.ZCODE_AGENT_SERVER_ARGS_JSON) ?? ["app-server", "--stdio"],
        cwd: process.env.ZCODE_AGENT_SERVER_CWD?.trim() || context.workspacePath,
      },
      context.presentationSurface,
    );
  }

  // 顺序:env 显式覆盖 → monorepo dev 源码/dist(dev 改源码立刻生效,不会被远端历史装的 native binary
  // 抢先匹配)→ 桌面打包态 Electron Node runtime 跑 zcode.cjs → 已部署 native binary(远端 SSH 兜底)。
  const bundled =
    resolveBundledWorkspaceZCodeAgentCommand(context) ??
    resolveElectronRuntimeZCodeAgentCommand(context);
  return applyPresentationSurfaceToCommand(
    bundled
      ? { ...bundled, supportsStorageStartup: true }
      : resolveDeployedZCodeAgentBinaryCommand(context),
    context.presentationSurface,
  );
}
  • 打包态:Host 跑在 Electron utility process 里,process.execPath 指向 Electron Helper,于是直接用它加 ELECTRON_RUN_AS_NODE=1 执行 resources/glm/zcode.cjs;注释说这样不必再随包带一份 Node,体积从约 180 MB 降到约 16 MB(zcodeAgentProcessManager.ts:412zcodeAgentProcessManager.ts:415)。不设这个变量,子进程会被当成 Chromium 子进程卡在 GPU 初始化(zcodeAgentProcessManager.ts:433)。
  • 开发态:向上找仓库里的 apps/zcode-cli/packages/cli/dist/zcode.cjs,找不到 dist 就用 tsx 直接跑 src/main.tszcodeAgentProcessManager.ts:353zcodeAgentProcessManager.ts:379)。
  • 参数:默认 app-server --stdiopackages/shared/src/zcode-agent-runtime.ts:31)。Host 的装配事实是桌面本地宿主或挂在桌面上的远端、且灰度开关没有关掉时,再追加 --surface desktoppackages/services/src/zcode-agent/zcodeAgentPresentationSurface.ts:12zcodeAgentProcessManager.ts:466),CLI 把它解析为 zcode_desktop 呈现面(apps/zcode-cli/packages/cli/src/run.ts:146)。
  • 环境:清洗后的 process.env,加运行环境标记、设置页的代理等 spawn 期变量、命令自带变量,再加 ZCODE_WORKSPACE_IDENTITY;POSIX 下以独立进程组启动,便于整棵树回收(zcodeAgentProcessManager.ts:1019packages/services/src/runtime-tools/agentProxyEnv.ts:116)。

进程池以工作区 key 为键,同一工作区的并发 getClient 收敛到同一个启动中的 Promise(zcodeAgentProcessManager.ts:839zcodeAgentProcessManager.ts:859),一个 Agent 进程里跑该工作区的全部会话。会话之外,Host 还有两条控制面泳道,都以数据目录下固定的 .zcode/plugin-workspace 作工作目录(packages/services/src/zcode-agent/zcodeAgentService.ts:464):插件市场管理,请求超时放宽到 5 分钟(zcodeAgentService.ts:1076zcodeAgentService.ts:325);查询 MCP 状态的 mcp-status,空闲 5 分钟自动回收,单独成进程是因为 mcp/list 的慢握手会堵住串行的 stdio 队列(zcodeAgentService.ts:1083zcodeAgentService.ts:322)。

Agent 也会反过来向 Host 发请求,Host 处理的有:session/requestRuntimePreferencesinteraction/requestPermissioninteraction/requestUserInputinteraction/requestProviderRuntimeHeadersinteraction/requestOfficialMcpAuthHeadersinteraction/browserListinteraction/browserExecuteautomation/createautomation/listoffPeak/createoffPeak/listzcodeAgentService.ts:2103 起,方法名见 packages/shared/src/zcode-protocol/index.ts:3567zcode-protocol/index.ts:3641zcode-protocol/index.ts:3657)。

stdio 与 stderr

stdout 上一行一帧 JSON。分帧只认 LF,不用 readline,因为它会把 U+2028、U+2029 当成换行,把含这类字符的模型文本切成半帧(packages/services/src/zcode-agent/zcodeStdioTransport.ts:61)。每帧都过 zod 校验,解析失败直接把传输判为关闭(zcodeStdioTransport.ts:244)。

stderr 有自己的收集器,寿命独立于协议(packages/services/src/zcode-agent/agentStderrCollector.ts:6)。每行先脱敏(api_key=Bearersk- 开头的裸 Key 等),截到 1000 字符,只保留最后 20 行(zcodeAgentProcessManager.ts:246);能解析成结构化诊断的行单独作为运行期异常上报(zcodeAgentProcessManager.ts:1038)。进程非预期退出时,先等 stderr 排空(默认 250 毫秒,agentStderrCollector.ts:4),再把尾部 20 行随退出事件写进错误日志(zcodeAgentProcessManager.ts:1239)。

超时、退出与“重启”

请求默认超时 3 分钟(packages/services/src/zcode-agent/zcodeProtocolClient.ts:48)。业务请求超时说明这条连接已不可信,管理器会淘汰这个客户端并回收整棵进程树,只有取消通知超时是例外(zcodeAgentProcessManager.ts:1280zcodeAgentProcessManager.ts:1293)。

代码里没有“崩溃后自动重启”的循环。协议关闭或进程退出时,条目从进程池摘掉(zcodeAgentProcessManager.ts:1312),下一次 getClient 才拉起新一代;运行时身份是“工作区 key、代次、pid、泳道”拼成的字符串(zcodeAgentProcessManager.ts:1089)。新代次只有在收到真实的 spawn 事件后才广播“运行时已重启”,注释说此前在 spawn 失败时也广播,引发“失败启动、假重启、重连”的自激风暴(zcodeAgentProcessManager.ts:1137)。订阅方据此重订 v4 主题,因为订阅活在 CLI 进程内存里,换代即失效(zcodeAgentProcessManager.ts:202)。

正常关闭先给 stdin 发 EOF,这是 app-server --stdio 的自然退出边界;最多等 1.8 秒,再按预先保存的进程树快照回收后代,强杀前留 2 秒(zcodeStdioTransport.ts:27zcodeStdioTransport.ts:152)。

存储启动门

开发态与打包态的内置 Agent 命令都标了 supportsStorageStartup(见上面代码里的 bundled 分支),远端部署的命令不标。这类 Agent 启动时先打开自己的 SQLite 会话库(需要时迁移),并用 startup/storageState 通知逐阶段报告(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/storage-startup.ts:40);ZCodeStorageStartupGate 只接受同一次尝试、同一个库、序号递增的状态帧,30 秒内没有第一帧就以 startup_status_timeout 失败(packages/services/src/zcode-agent/zcodeStorageStartupGate.ts:9zcodeStorageStartupGate.ts:38)。门没开之前,业务请求连请求对象和超时计时器都不创建(zcodeProtocolClient.ts:141);状态为失败时,所有挂起请求一并拒绝(zcodeProtocolClient.ts:311)。空闲回收也会避开迁移中的进程(zcodeAgentProcessManager.ts:641)。

任务列表与 Agent 会话库

会话的权威数据在 Agent 的 SQLite 会话库里(见SQLite 会话库)。侧栏的任务列表另有一个库 ~/.zcode/v2/tasks-index.sqlitepackages/services/src/paths.ts:185),存未读、分组、归档、全文检索文本这些“产品壳”状态,多窗口的 Host 共用它。两边靠 ZCodeTaskIndexSyncer 对齐:为每个工作区订阅 v4 的 sessions-indexworkspace-config 两个主题,把 CLI 投影出的终态、标题与配置目录落到索引库,再广播 workspace_task_list_changedpackages/services/src/zcode-agent/zcodeTaskIndexSyncer.ts:94)。

图表加载中…

每条会话摘要的处理规则集中在 processSummaryzcodeTaskIndexSyncer.ts:669):

    // draft 裁决:纯内存态、不落盘,也绝不进 task index sqlite。
    if (next.phase === "draft") {
      return;
    }
    const target = sessionTargetFrom(state.target, next.sessionId);
    const becameVisibleTask = previous === undefined || previous.phase === "draft";
    // 终态迁移 = 基线里真实观察到非终态 → 终态。无基线的会话(冷恢复 hydration、
    // 断档降级后新出现的历史会话)不回放终态;活跃会话必先以 running/prewarming
    // 进入基线(gateway 每个事件都 fan-out),不会漏掉真实收口。
    const becameTerminal =
      previous !== undefined && !isTerminalPhase(previous.phase) && isTerminalPhase(next.phase);
    if (becameTerminal) {
      applyTerminalTransition(target, next, {
        moveGroupedTaskToTop: becameVisibleTask,
      });
      return;
    }
    if (becameVisibleTask) {
      // v4 预热 session 从 draft 提升,或 online delta 首次出现新 session 时,
      // 不经过 zcodeSessionService.createSession。此处是最早且不依赖标题时序的新任务边界;
      // 立即回源写入 task 行与 grouped root 最小 sort_order,避免缺序节点落到末尾。
      void resyncTaskIndexRowFromAgent(target, "session.became-visible", {
        moveGroupedTaskToTop: true,
      });
      return;
    }
  • 首帧快照只做“缺了才插”,每批 64 行,不广播、不回放历史终态;注释说纯 v4 界面不走旧的初始化路径,若把首帧当成已有存量的静默基线,远端新库会永远是 0 行(zcodeTaskIndexSyncer.ts:701zcodeTaskIndexSyncer.ts:56)。
  • 同步器是被动观察者,不能为了订阅而拉起 CLI:没有运行时的工作区只留一个休眠占位,等进程管理器报告“可用”才订阅,“不可用”时只清本地订阅关系(zcodeTaskIndexSyncer.ts:1655zcodeTaskIndexSyncer.ts:1548zcodeTaskIndexSyncer.ts:1569)。
  • 写路径另有补充:zcodeSessionService 在创建、恢复会话与切换模型之后主动同步一次快照,让侧栏立刻拿到标题和更新时间(zcodeTaskIndexSyncer.ts:113)。
  • 同步器还对外发一个“会话就绪”事件,手机远控的命令队列以它作为发送下一条的边界(zcodeTaskIndexSyncer.ts:167),见远程工作区与手机远控

Agent 的 V8 字节码

仓库里有一条把 Agent 编译成 V8 字节码运行的路径,但目前只是开发态试验。入口是 pnpm dev:desktop:bytecodepackage.json:12),它设 ZCODE_DESKTOP_AGENT_BYTECODE=1,并在构建 Agent 之后多跑一步 build-desktop-agent-bytecode.mjsscripts/dev-desktop-env.mjs:29dev-desktop-env.mjs:65);开启 E2E 覆盖率时拒绝执行,脚本自称“字节码试验”(scripts/build-desktop-agent-bytecode.mjs:31)。

怎么编。必须用当前 Electron 以 Node 模式编译(build-desktop-agent-bytecode.mjs:36):把 zcode.cjsModule.wrap 包成 CommonJS 函数,交给 vm.Script,只调用 createCachedData(),不执行模块,避免触发 CLI 的存储、网络和进程副作用(scripts/compile-desktop-agent-bytecode.cjs:15)。编译器和加载器共用一处配置 --no-lazy --no-flush-bytecode:字节码不带可重新编译的源码,函数必须一次编完,也不能被 V8 回收后再从源码重编(scripts/desktop-agent-bytecode-runtime.cjs:10)。产物是三个文件:以摘要命名的 zcode.bytecode-<sha256>.jsc、以内容摘要命名的运行时脚本,以及内嵌元数据的加载器 zcode.bytecode.cjs;前两个按不可变文件写入,加载器最后原子替换,失败时上一版加载器仍能找到自己的字节码(build-desktop-agent-bytecode.mjs:41build-desktop-agent-bytecode.mjs:48)。

怎么跑。加载器的核心在 desktop-agent-bytecode-runtime.cjs:26

async function loadBytecode(metadata, targetModule, targetRequire) {
  const runtime = configureBytecodeRuntime();
  if (JSON.stringify(runtime) !== JSON.stringify(metadata.runtime)) {
    throw new Error(
      "字节码运行时不匹配,请用当前 Electron 重新运行 pnpm build:desktop-agent:bytecode",
    );
  }
  const directory = dirname(targetModule.filename);
  if (basename(metadata.bytecodeFile) !== metadata.bytecodeFile) {
    throw new Error("无效的字节码文件名");
  }
  const cachedData = await readFile(join(directory, metadata.bytecodeFile));
  if (bytecodeDigest(cachedData) !== metadata.bytecodeSha256) {
    throw new Error("字节码摘要不匹配,请重新构建桌面 Agent");
  }
  // 使用 ASCII 空格而非双字节零宽字符。这里只消除明文源码,仍保留等长占位内存。
  const source = " ".repeat(metadata.sourceLength);
  const filename = join(directory, metadata.sourceFile);
  const script = new vm.Script(source, {
    cachedData,
    filename,
    // 动态 import 必须交回 Node,继续按原 bundle 的 URL 解析外置 ESM 与原生依赖。
    importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
  });
  if (script.cachedDataRejected) {
    throw new Error("V8 拒绝字节码缓存,请重新构建桌面 Agent");
  }

运行时指纹包括 Electron、Node、V8 版本、平台、架构和 cachedDataVersionTag,任何一项不同都拒绝加载(desktop-agent-bytecode-runtime.cjs:12)。从加载器的做法看,V8 只要求占位源码与原文等长,于是内存里是一串空格而不是明文代码。

为什么。代码没有写成一句话的动机,但痕迹一致:加载器注释说“只消除明文源码”;桌面端的 tsup 配置在生产构建里压缩 main、host、preload,理由是“增加逆向和内部实现暴露风险”(packages/desktop/tsup.config.ts:73);桌面 Agent 的构建同样压缩、不带 sourcemap(apps/zcode-cli/packages/cli/scripts/build.mjs:128)。字节码是在压缩之外再去掉明文。需要注意,它目前只接在开发态的仓库路径上(zcodeAgentProcessManager.ts:358):打包暂存只复制 zcode.cjspackages/desktop/scripts/stage-agent-bundle.mjs:41),安装包里跑的仍是压缩后的 JS。存储准备用的 Worker 也固定走 JS 入口,因为 Worker 与 Electron Node 子进程的 V8 快照可能不同(zcodeAgentProcessManager.ts:370)。

内嵌浏览器

内置浏览器是 Renderer 里的 <webview> 标签,分区是 persist:zcode-embedded-browserpackages/desktop/src/main/browserDataManager.ts:30),所以主窗口开了 webviewTagpackages/desktop/src/main/desktopWindowChrome.ts:586)。每个 webview 挂载前,Main 在 will-attach-webview 里强制指定 preload,打开 contextIsolationsandbox,删掉页面自带的 preload 和 disablewebsecuritydesktopWindowChrome.ts:640)。

Agent 的 Browser Use 最终也落到这些 webview 上:Agent 发 interaction/browserExecute 给 Host,Host 经 parentPort 转给 Main,单条命令预算 30 秒,比外层 node_repl 工具的 60 秒短,给收尾留余量(packages/desktop/src/host/browserControlMainBridge.ts:50);Main 的 runBrowserCommandOnView 交给 browserGuestManager.executepackages/desktop/src/main/index.ts:453main/index.ts:497)。管理器只接受真正的 <webview> guest,拒绝其他类型,以免 CDP 输入打到 ZCode 自己的输入框上(packages/desktop/src/main/browserView/browserGuestManager.ts:550),然后用 webContents.debugger.attach("1.3") 走 CDP(browserGuestManager.ts:696)。DOM 快照借用 Playwright 1.59 生成的注入脚本:从 playwright-core 里只取出那段字符串字面量并做完整性检查,不运行 Playwright 本身(packages/desktop/src/main/browserView/playwrightInjectedScriptSource.ts:32)。Agent 这一侧怎样用 JS REPL 驱动浏览器,见node_repl、Browser Use 与 Computer Use

更新、终端与历史导入

自动更新用 electron-updater,但 feed 换成了自定义的 ManifestUpdateProvider:向服务端 /api/v1/releases/electron/manifest 取清单,平台写成 darwin-aarch64 这样的形式,渠道 stable 与 preview 分别映射为 13packages/desktop/src/main/manifestUpdateProvider.ts:22manifestUpdateProvider.ts:36manifestUpdateProvider.ts:70)。每小时检查一次(packages/desktop/src/main/autoUpdater.ts:26);自动下载关闭,先比较远端版本与已下载版本再决定;Windows 上不在退出时自动安装,避免用户紧接着关机把安装器打断(autoUpdater.ts:1501)。打包版忽略 ZCODE_UPDATE_FEED_URL--zcode-update-feed-url 这两个开发用覆盖(autoUpdater.ts:707)。electron-builder 里的 generic 发布地址只是占位,并关掉多 Range 请求,好让 Windows 差分更新不退化成整包下载(packages/desktop/electron-builder.config.js:756)。启动时另有强制升级检查,读的是 /api/v1/client/configspackages/desktop/src/main/forceUpdateGuard.ts:13)。

终端服务在 packages/services,跑在 Host 里(远端 server 里也有一份)。node-pty 延迟加载,注释说此前顶层导入会让缺少 pty.node 的远端在注册服务阶段就崩掉(packages/services/src/terminal/terminalService.ts:29);Windows 走 ConPTY(terminalService.ts:55)。打包时只解开目标平台的 node-pty 预编译目录(electron-builder.config.js:497)。

导入 Claude Code 会话。扫描的是真实用户 HOME 下的 ~/.claude/projects,与 ZCode 数据目录无关(packages/services/src/session/claude-native/claudeNativeSessionImportRepo.ts:63);原始 jsonl 复制到 ~/.zcode/v2/agent-config/claude/<工作区哈希>/projectsclaudeNativeSessionImportRepo.ts:270),工作区目录不存在或与当前工作区不符则跳过(packages/services/src/session/claude-native/claudeNativeSessionImportService.ts:73)。解析出的历史以 importedHistory 交给 Agent 创建一个真正的 ZCode 会话,这样切模型、续聊都能命中运行时,任务行另记 migrationSource: "claudeCode"packages/services/src/zcode-agent/zcodeTaskServiceAdapter.ts:2626)。

Main 的 mcpUserDirectory 负责读写两处用户级 MCP 配置:~/.zcode/cli/config.jsonmcp.servers~/.agents/mcp.jsonmcpServerspackages/desktop/src/main/mcpUserDirectory/index.ts:37),配置语义见 MCP

打包与签名

README 的打包命令(README.md:144):

pnpm bundle:desktop

# 指定目标平台与 CPU 架构
pnpm bundle:desktop -- --os win --arch x64

pnpm bundle:desktop -- --help

默认目标是 macOS arm64(packages/desktop/scripts/bundle.mjs:42)。脚本依次执行运行资源准备、构建、electron-builder、产物运行时依赖校验与体积审计(bundle.mjs:727)。资源准备包括远程资源(设 ZCODE_SKIP_REMOTE_ASSETS=1 可跳过)、Agent JS bundle、原生搜索工具,macOS 另编一个窗口位置辅助程序(packages/desktop/scripts/prepare-runtime-assets.mjs:29prepare-runtime-assets.mjs:51)。构建由 tsup 出 main、preload、host、scheduler 四份 bundle,Vite 出 Renderer;生产构建压缩、保留函数名、不带 sourcemap(packages/desktop/tsup.config.ts:67tsup.config.ts:136)。

electron-builder 配置取值
Electron 版本写死 41.0.3,语言包只留 en-US 与 zh-CN(electron-builder.config.js:468electron-builder.config.js:472
额外资源resources/glm 下的 Agent bundle、tools/ripgrep 与原生搜索工具、内置 Provider 配置(electron-builder.config.js:568
目标macOS 为 dmg 与 zip,Windows 为 NSIS,Linux 为 AppImage、deb、rpm、pacman;DMG 容量放大到 3200m(electron-builder.config.js:657electron-builder.config.js:734
协议注册 zcode:// 深链(electron-builder.config.js:649
afterPack把 pnpm 布局下容易漏拷的运行时依赖补进 app.asar,清掉 sourcemap 引用,校验原生包与 node-pty 预编译(electron-builder.config.js:541

签名默认关闭:只有 ZCODE_ENABLE_MAC_SIGN=1 且提供 APPLE_SIGNING_IDENTITYCSC_NAME 时才签名并启用 hardened runtime(electron-builder.config.js:82electron-builder.config.js:670)。公证不在 electron-builder 里做:macOS 产物先在 build 阶段签名,再由独立阶段公证(electron-builder.config.js:671);已预签名的 glmtools 目录列入 signIgnore,免得重复签名拖长时间(electron-builder.config.js:685)。本地构建因此是未签名的,README 给出的办法是装好后执行 sudo xattr -rd com.apple.quarantine /Applications/ZCode.appREADME.md:154)。

下一篇:Web 与服务端:同一套 UI 的另一种宿主——不开 Electron,同一套 @zcode/ui 怎样跑在浏览器里,服务又怎样经 HTTP 与 WebSocket 暴露。

本页目录