Electron + Python + Vue 打包桌面应用踩坑:把 Hermes Agent 做成跨平台客户端

48 阅读5分钟

Electron + Python + Vue 打包桌面应用踩坑:把 Hermes Agent 做成跨平台客户端

最近在研究 Nous Research 的 hermes-agent。它本身是一个 Python Agent 项目,支持自我改进、技能编写、工具调用,也可以接入一些外部消息渠道。

但实际体验下来,完整跑起来并不只是 pip install 这么简单。对于非 Python / Node 开发背景的用户来说,通常会遇到这些问题:

  • Python 环境和依赖安装
  • Web UI 的 Node 服务启动
  • .env 配置管理
  • 本地端口和进程管理
  • macOS / Windows / Linux 下的差异
  • 后台服务的启动、停止和日志排查

所以我尝试做了一个桌面端打包工程,把 Python Agent、Web UI 和 Electron 外壳组合到一起。本文主要记录这次打包过程中的技术方案和踩坑点。

整体架构

整体结构大概分为四层:

Electron 主进程
    ↓
本地 Koa Server
    ↓
Web UI / Vue 前端
    ↓
Python Agent Runtime

Electron 负责桌面应用生命周期、窗口管理和本地服务启动。Web UI 继续沿用原来的前端和 Koa 服务。Python Agent 则作为内置运行时的一部分随应用分发。

这样做的目标不是重写上游项目,而是尽量保持上游逻辑不变,只在外层补一层桌面端运行环境。

Python 运行时打包

Python 部分使用的是 python-build-standalone。它的好处是可以把 Python runtime 做成相对独立、可重定位的目录,适合随桌面应用一起分发。

依赖安装部分使用 uv 处理。构建阶段把 hermes-agent 相关依赖提前安装到随包目录里,运行时尽量不再要求用户本机存在 Python、pip 或 venv。

这里有几个需要注意的点:

  1. Python 路径不能写死成本机路径
  2. 运行时目录需要能在打包后的 app 结构里正确定位
  3. macOS .app、Windows 安装目录、Linux AppImage 的路径规则都不一样
  4. 子进程启动时要显式设置环境变量,避免吃到用户机器上的 Python 环境

这类问题在开发环境里不明显,但一旦打包后换机器运行,就很容易暴露。

Electron 内启动 Node 服务

Web UI 原本是一个 Node/Koa 服务。桌面端里我没有直接改成纯前端静态页面,而是让 Electron 主进程启动原来的 Koa server。

这里用到了:

ELECTRON_RUN_AS_NODE

这样可以让 Electron 以 Node 模式运行一段 server 代码,然后 BrowserWindow 加载本地地址:

http://127.0.0.1:8648

这个方案的优点是对原 Web UI 改动较小,缺点是要额外处理:

  • server 启动时机
  • 端口占用
  • 子进程退出
  • 日志收集
  • 应用关闭时的进程回收

桌面应用里最怕的不是服务启动失败,而是失败后用户完全不知道发生了什么。所以日志路径、错误弹窗和重启逻辑都需要补上。

自动登录和弹窗处理

Web UI 原来更偏开发部署场景,放进桌面端以后,一些首次启动流程会显得比较突兀。

我做了一些 bundle 级别的 patch,例如:

  • 首次启动时自动创建默认本地配置
  • 压制不适合桌面端的弹窗
  • 启动后自动进入主界面
  • 配置变更后自动重启本地服务

这些逻辑本身不复杂,但比较考验边界处理。比如配置写入失败、端口还没释放、服务重启过程中用户再次点击等,都需要做幂等。

macOS 下的 socket 问题

这次最诡异的一个问题出在 macOS。

原本 agent-bridge 某些链路使用的是 unix socket。在部分机器上,未签名 Electron 应用里的子进程会被系统安全策略或安全软件干预,表现为 socket 文件存在,但连接失败,或者子进程被静默杀掉。

最后我把 bridge 相关链路改成 TCP loopback,统一走本地端口通信。这样做虽然没有 unix socket 那么“干净”,但跨平台一致性更好,也更容易排查问题。

这个问题的经验是:桌面端分发环境和开发环境差异很大。尤其是 macOS 上,未签名应用、子进程、socket、文件权限、安全软件叠在一起,会出现很多看起来不符合直觉的问题。

自动更新和 Release 构建

自动更新使用的是 electron-updater,产物放在 GitHub Releases。

CI 部分一开始踩了一个矩阵构建的坑。多平台、多架构同时跑时,如果每个 matrix job 都尝试 publish,很容易出现竞态:

  • 多个 job 同时创建 draft release
  • artifact 名称冲突
  • 部分平台产物覆盖
  • release 状态不一致

最后改成了两阶段:

  1. matrix job 只负责构建并上传 artifact
  2. 最后一个 publish job 统一下载 artifact 并发布

这样 release 流程稳定很多,也方便失败后单独重跑。

一些经验总结

这次打包过程中最大的感受是:把一个开发者工具包装成桌面应用,难点不在 Electron 本身,而在运行环境的收敛。

尤其是这几类问题:

  • Python / Node 双运行时如何随包分发
  • 子进程如何稳定启动和退出
  • 用户机器上的环境变量如何隔离
  • macOS、Windows、Linux 路径差异如何处理
  • 本地服务日志和错误如何暴露给用户
  • CI 多平台产物如何避免竞态

如果只是本机跑通,难度不高;但要做到换一台干净机器还能跑起来,就需要把很多隐式依赖都显式处理掉。

相关项目

这次桌面端打包主要基于两个上游项目:

  • Nous Research hermes-agent
  • EKKO Learn AI hermes-web-ui

我这边主要做的是桌面端打包、运行时整合、启动流程和跨平台构建相关工作。后续如果继续迭代,重点应该会放在日志可视化、配置管理、首次启动体验和更稳定的自动更新上。