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。
这里有几个需要注意的点:
- Python 路径不能写死成本机路径
- 运行时目录需要能在打包后的 app 结构里正确定位
- macOS
.app、Windows 安装目录、Linux AppImage 的路径规则都不一样 - 子进程启动时要显式设置环境变量,避免吃到用户机器上的 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 状态不一致
最后改成了两阶段:
- matrix job 只负责构建并上传 artifact
- 最后一个 publish job 统一下载 artifact 并发布
这样 release 流程稳定很多,也方便失败后单独重跑。
一些经验总结
这次打包过程中最大的感受是:把一个开发者工具包装成桌面应用,难点不在 Electron 本身,而在运行环境的收敛。
尤其是这几类问题:
- Python / Node 双运行时如何随包分发
- 子进程如何稳定启动和退出
- 用户机器上的环境变量如何隔离
- macOS、Windows、Linux 路径差异如何处理
- 本地服务日志和错误如何暴露给用户
- CI 多平台产物如何避免竞态
如果只是本机跑通,难度不高;但要做到换一台干净机器还能跑起来,就需要把很多隐式依赖都显式处理掉。
相关项目
这次桌面端打包主要基于两个上游项目:
- Nous Research hermes-agent
- EKKO Learn AI hermes-web-ui
我这边主要做的是桌面端打包、运行时整合、启动流程和跨平台构建相关工作。后续如果继续迭代,重点应该会放在日志可视化、配置管理、首次启动体验和更稳定的自动更新上。