przbadu/hermes-ui
GitHub: przbadu/hermes-ui
Hermes gateway 的浏览器端 PWA 前端,从桌面 Electron 应用提取为独立 Vite Web 应用,提供聊天、模型选择等界面并通过 REST/WebSocket 与后端 gateway 通信。
Stars: 51 | Forks: 6
# hermes-ui
`hermes-ui` 是 Hermes gateway 之上的一个轻量级浏览器 UI 封装。
它是官方的 Hermes 桌面渲染器,从 `hermes-agent` monorepo 中提取出来,并重新打包为一个普通的 Vite Web 应用,这样同一套 UI 就可以在浏览器中运行,而不是通过 Electron。
它本身不提供后端:每个请求(REST、auth 和 WebSocket)都会发送到运行中的 Hermes gateway。
## 演示
这是一个针对 Hermes gateway 运行 UI 的简短演示——包括登录、聊天、模型选择器、主题和设置。
有关提取计划和来源信息,请参见:
- [PLAN.md](PLAN.md) - 本仓库背后的计划。
- [UPSTREAM.md](UPSTREAM.md) - 上游 commit、更改内容以及如何重新同步。
## 仓库布局
- `app/` - Vite Web 应用 (React + TypeScript)。构建输出到 `app/dist/`。
- `shared/` - 由 `app/` 通过 `@hermes/shared` 别名使用的共享 TypeScript 源码。
- `bin/dev`, `bin/prod` - 零配置运行器(安装、构建并启动只需一条命令)。
- `UPSTREAM.md`, `LICENSE` - 来源和许可证 (MIT, Copyright (c) 2025 Nous Research)。
- `scripts/serve-on-gateway.sh` - 辅助脚本,用于将 gateway 指向构建好的 `app/dist/` 并启动它。
## 前置条件
- [bun](https://bun.sh) 作为包管理器和脚本运行器。
- 一个运行中的 Hermes gateway(`hermes serve` 或 `hermes dashboard`,FastAPI,默认端口 `9119`)。
## 快速开始
这两个运行器会安装依赖并启动应用,无需手动设置。
```
cd $HOME/dev/hermes-apps/hermes-ui
./bin/dev # dev server with hot reload; open http://127.0.0.1:5174
./bin/prod # build, then serve the bundle from the gateway (same-origin)
```
`bin/dev` 默认代理到 `http://127.0.0.1:9119`;可以通过 `HERMES_GATEWAY_URL=http://host:port ./bin/dev` 进行覆盖。
`bin/prod` 会将任何额外的参数传递给 `hermes serve`,例如 `./bin/prod --port 9200`。
对于使用 OAuth 的 gateway,推荐使用 `bin/prod`:登录重定向必须返回到提供应用服务的同一个 origin,而 gateway 托管的路径保证了这一点。
gateway 旨在回环地址(`127.0.0.1`,默认值)上运行。请在同一台机器上运行 UI 和 gateway,并通过 `127.0.0.1` 访问它。
## 自动化设置(适用于 Hermes / AI agents)
如果您是 AI agent(Hermes、Claude Code 等),或者您正要求一个 agent 来进行设置,请遵循 [AGENTS.md](AGENTS.md)。
这是一份循序渐进的操作指南,涵盖了预检、构建、从 gateway 提供资源包服务(手动路径)、验证以及重新构建。
从仓库根目录开始的简短版本如下:
```
cd app && bun install && bun run build # build the static bundle
cd .. && ./bin/prod # serve it from the gateway (same-origin)
```
该手册特意仅涵盖手动路径;除非您明确要求,否则 agent 不应安装或修改 systemd 服务。
## 在应用内选择 gateway
可以在运行时通过“设置 -> Gateway”编辑连接,就像桌面应用一样。
它默认使用提供应用服务的 origin,因此 gateway 托管路径和开发代理路径无需任何配置。
您可以将其指向另一个 gateway URL(提供服务的 origin 上的绝对 `https://host` 或 `/prefix` 路径),并选择 token 或 OAuth 身份验证;该选择会保存在浏览器中。
下面提到的同源限制仍然适用于您输入的任何 URL。
您可以保存多个 gateway(个人、公司等)并在它们之间切换;该列表存储在浏览器中。
### 在 `config.json` 中将 gateway 加入白名单
`HERMES_GATEWAY_URL` 用于选择开发服务器进行同源代理的默认 gateway。
要让其他 gateway 表现一致——即进行同源代理,以便同时支持 token 和 OAuth——请将 `config.example.json` 复制到仓库根目录下的 `config.json`,并列出它们的 URL:
```
{
"gateways": [
"http://203.0.113.10:9119",
"https://hermes.example.com"
]
}
```
`config.json` 已被 git 忽略,因此它绝不会将您的 gateway URL 泄露到版本控制中。
您也可以通过环境变量传递它们:`HERMES_GATEWAY_WHITELIST=url1,url2 bin/dev`。
开发服务器会将这些地址与 `HERMES_GATEWAY_URL` 合并到一个白名单中,并将其交给应用。
然后,每个被列入白名单的 gateway 都会被归并到提供服务的 origin 下:开发代理将每个请求(以及 WebSocket)路由到当前活动的 gateway,该 gateway 是根据经过验证的标记按请求选择的,因此只有白名单中的 origin 才能被访问(而非开放代理)。
由于是同源代理,在“设置”中添加白名单 gateway 时的连接方式与默认 gateway 完全一致——包括 OAuth 登录和 cookie 会话。
这**不会**向您保存的 gateway 列表中添加任何内容——您仍然需要自己在“设置”中添加每个 gateway;白名单仅决定开发代理将承载哪些 gateway。
这是一种开发时机制:`bin/dev` (Vite) 会注入白名单,而 gateway 托管的生产路径不会读取 `config.json`。
回环 gateway(`localhost` / `127.0.0.1`)始终被允许,并且永远不需要在 `config.json` 中添加条目。
有两个值得注意的注意事项:
- **OAuth 需要 gateway 信任开发 origin。** 由于代理保留了开发环境的 Host,gateway 会将其 OAuth 的 `redirect_uri` 构建为 `http://127.0.0.1:5174/auth/callback`。只有当该 gateway 的 OAuth 提供商信任 `http://127.0.0.1:5174` 作为重定向 URI 时,登录才能完成(本地默认 gateway 以及您可以自行配置的自托管 gateway 可以做到;而如果托管的 gateway 仅注册了其自身的 https 回调,则在 `/login` 处会因 `redirect_uri_mismatch` 而失败)。对于不信任开发 origin 的 gateway,请改用**会话 token**。
- **通过其 http origin/IP 访问远程 gateway,而不是 https 域名。** 代理到 `https://` gateway 域名(特别是由 CDN 前置的域名)可能会导致 Node 端的 TLS 握手失败(`SSL alert number 40`)。列出 gateway 的 `http://host:port` origin 可以避免这种情况,这也是自托管 gateway 推荐的格式。
每个 gateway 的会话 cookie 都在开发 cookie jar 中按目标进行了命名空间隔离,因此切换 gateway 绝不会将一个 gateway 的会话泄露给另一个,并且切换回来时会保留之前的会话。
已知限制:在开发环境下,不支持将两个浏览器标签页指向两个不同的活动 gateway(因为活动 gateway 存在于共享的浏览器存储中);如果需要同时使用两个,请为每个浏览器配置文件使用一个 gateway。
## 为什么强制要求同源
gateway 被锁定为只能由同源浏览器访问,无法进行跨源通信:
- CORS 被硬编码为 localhost origin,因此从任何其他 origin 提供服务的浏览器都会被拒绝。
- 会话 cookie 是 `SameSite=Lax`,因此它们不会在跨站请求中发送。
- WebSocket 会拒绝外部 origin,并以关闭代码 `4403` 关闭连接(参见 `hermes-agent/hermes_cli/web_server.py:15308`)。
直接从 gateway 提供构建好的资源包服务(选项 A)可以使 UI、REST、auth 和 WebSocket 共享同一个 origin,因此 cookie、CORS 和 WebSocket 无需额外配置即可正常工作。
开发代理(选项 B)和反向代理(选项 C)是在不直接托管在 gateway 上的情况下,保持这种同源属性的两种方法。
### 例外情况:回环 gateway
当应用本身从回环地址(`localhost` 或 `127.0.0.1`)提供服务时,它可以直接访问任何其他回环 gateway,即使端口不同,也无需代理。
这些 origin 属于同站(仅端口不同),并且浏览器所需的一切都已就绪:gateway 的 CORS 允许 localhost origin,应用在 REST 请求的 `X-Hermes-Session-Token` 标头和 WebSocket 的 `?token=` 查询参数中发送其凭据,因此永远不需要跨源传递任何 cookie。
这就是为什么 localhost gateway 一旦在“设置 -> Gateway”中添加就能正常工作,并且不需要在 `config.json` 中添加条目即可访问的原因。
OAuth cookie 会话是唯一仍然不能直接跨源传递的内容:gateway 不发送 `Access-Control-Allow-Credentials`,因此其 `SameSite=Lax` 会话 cookie 永远不会在跨源情况下被接受。
对于您希望通过 OAuth 登录的跨端口回环 gateway,请将其加入白名单(参见[在 `config.json` 中将 gateway 加入白名单](#whitelisting-gateways-in-configjson)),以便开发代理将其归并为同源,或者使用**会话 token**。
上述全面的同源要求仍然适用于未通过开发代理加入白名单的任何非回环 origin。
## gateway 如何托管资源包
gateway 的 `mount_spa()` 提供由 `HERMES_WEB_DIST` 环境变量选择的静态目录服务:
- 该目录从 `HERMES_WEB_DIST` 读取,如果未设置该变量,则回退到内置的 `web_dist`:
`WEB_DIST = Path(os.environ["HERMES_WEB_DIST"]) if "HERMES_WEB_DIST" in os.environ else Path(__file__).parent / "web_dist"` (`hermes-agent/hermes_cli/web_server.py:121`)。
- 资源包的 `assets` 子目录挂载在 `/assets` 下,因此 gateway 期望 `/assets` 存在:
`application.mount("/assets", StaticFiles(directory=WEB_DIST / "assets"), name="assets")` (`hermes-agent/hermes_cli/web_server.py:15678`)。
- 所有其他路径都会回退到 `/index.html`,以实现 SPA 的客户端路由 (`hermes-agent/hermes_cli/web_server.py:15694-15703`)。
- 在提供 `index.html` 服务之前,gateway 会将会话 token 作为 `window.__HERMES_SESSION_TOKEN__` 注入到位于 `` 之前的 `