Hermes Web UI 外网访问与 Android App 连接:ngrok 反向隧道实战

1:结论与适用范围

这篇文章记录一次在 Windows + WSL2 环境中,将本机 Hermes Web UI 通过 ngrok 发布为 HTTPS 地址,并用 Android 的 Hermes Agent WebUI App 连接访问的完整过程。

最终结构如下:

1
2
3
4
5
6
7
8
9
Android Hermes Agent WebUI App / 手机浏览器
                ↓ HTTPS
        https://<随机子域>.ngrok-free.dev
                ↓ ngrok 出站反向隧道
          WSL: ngrok-agent(低权限账户)
                ↓ 仅访问 localhost
        127.0.0.1:8648(Hermes Web UI)
        Hermes Gateway: 127.0.0.1:8642

这个方案的特点:

  • 不需要公网 IPv4、路由器端口映射或自购域名;
  • 本机主动向 ngrok 云端发起 HTTPS 出站连接;
  • 手机可通过普通 HTTPS URL 访问;
  • Hermes Web UI 保持绑定在 127.0.0.1,不直接暴露 8648
  • Android App 可作为 WebView 容器访问 Web UI;
  • 最终只使用 Hermes Web UI 自身的 token 登录,不再叠加 ngrok OAuth 或 Basic Auth。

本文适合个人远程管理 Hermes。Hermes 具备本机工具调用能力,不应把数据库、SSH、券商接口、OEMS/FIX/SBL/LMS 等端口通过 ngrok 一并发布。

2:先理解:Tailscale、ngrok 与 FRP 的选择

方案是否公网可访问是否需手机 App是否需要 VPS本次用途
Tailscale Serve否,仅 tailnet安全性最佳,适合本人设备访问
ngrok HTTP Tunnel是,HTTPS URL否,浏览器/App 都可本次采用,部署最快
FRP是,需公网 frps长期自建、可控性更高
Cloudflare Tunnel + Access是,但可加身份网关长期公网入口的优选方案

如果只是本人手机访问,优先考虑 Tailscale;如果希望 Android App 或普通浏览器直接填 HTTPS 地址,ngrok 的接入成本更低。

3:前置条件

3.1 Hermes Web UI 必须只监听 localhost

本次环境中,Web UI systemd 服务明确绑定:

1
2
3
# /etc/systemd/system/hermes-web-ui.service
Environment=BIND_HOST=127.0.0.1
ExecStart=/usr/local/bin/hermes-web-ui-systemd 8648

检查命令:

1
2
3
systemctl is-active hermes-web-ui.service
ss -ltnp | grep ':8648'
curl -I http://127.0.0.1:8648/

预期看到类似:

1
2
LISTEN ... 127.0.0.1:8648 ...
HTTP/1.1 200 OK

不要为了手机访问把 Web UI 改成 0.0.0.0:8648;ngrok 可以直接代理 127.0.0.1:8648

3.2 ngrok 账户与专用 Authtoken

  1. 注册/登录 ngrok Dashboard;
  2. 在 Dashboard 创建一个专用于此 WSL 的 Authtoken;
  3. 不要把 Authtoken 写入 Git、Markdown、聊天记录或 shell history;
  4. 为了降低凭据泄露影响,一台机器使用一个独立 token。

ngrok 的工作模式是:Agent 从 WSL 主动连接到 ngrok 云端的 443 端口,ngrok 再将公网 HTTPS 请求通过加密隧道转回本机。因此不需要开放家庭路由器端口。

4:安装 ngrok Agent

Ubuntu/WSL 可通过 ngrok 官方 APT 源安装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
  -o /etc/apt/keyrings/ngrok.asc
sudo chmod 0644 /etc/apt/keyrings/ngrok.asc

printf '%s\n' \
  'deb [signed-by=/etc/apt/keyrings/ngrok.asc] https://ngrok-agent.s3.amazonaws.com buster main' \
  | sudo tee /etc/apt/sources.list.d/ngrok.list >/dev/null

sudo apt-get update
sudo apt-get install -y ngrok
ngrok version

安装后应能看到 ngrok version 3.x

5:以低权限 systemd 服务运行 ngrok

不建议用 root 直接在终端长期执行:

1
ngrok http 8648

原因包括:进程断开后隧道消失、缺少开机重启、凭据边界不清晰、日志不便于审计。

5.1 创建专用账户与目录

1
2
3
4
5
6
sudo useradd --system \
  --home-dir /nonexistent \
  --shell /usr/sbin/nologin \
  --user-group ngrok-agent

sudo install -d -o root -g ngrok-agent -m 0750 /etc/ngrok-hermes

Authtoken 单独保存在受限文件中。下面命令不会把 token 写入 shell history:

1
2
3
4
5
6
read -rsp 'Paste ngrok authtoken: ' token; printf '\n'
sudo install -o root -g ngrok-agent -m 0640 /dev/null \
  /etc/ngrok-hermes/authtoken.env
sudo sh -c 'printf "NGROK_AUTHTOKEN=%s\n" "$1" > /etc/ngrok-hermes/authtoken.env' \
  sh "$token"
unset token

文件权限应为:

1
2
sudo stat -c '%a %U:%G %n' /etc/ngrok-hermes/authtoken.env
# 预期:640 root:ngrok-agent /etc/ngrok-hermes/authtoken.env

5.2 ngrok service 文件

创建 /etc/systemd/system/ngrok-hermes.service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[Unit]
Description=ngrok HTTPS gateway for Hermes Web UI
Documentation=https://ngrok.com/docs/guides/share-localhost/tunnels
Wants=network-online.target
After=network-online.target hermes-web-ui.service
Requires=hermes-web-ui.service
ConditionPathExists=/etc/ngrok-hermes/authtoken.env

[Service]
Type=simple
User=ngrok-agent
Group=ngrok-agent
EnvironmentFile=/etc/ngrok-hermes/authtoken.env
ExecStart=/usr/local/bin/ngrok http http://127.0.0.1:8648 --name hermes-web-ui --traffic-policy-file /etc/ngrok-hermes/traffic-policy.yml --inspect=false --log stdout --log-format json
Restart=on-failure
RestartSec=10s
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target

其中:

  • User=ngrok-agent:Agent 无登录 shell,降低被利用后的权限范围;
  • http://127.0.0.1:8648:隧道只连接 Web UI 的 localhost 地址;
  • --inspect=false:关闭 Agent 本地 HTTP Inspector;
  • Restart=on-failure:网络短暂中断后由 systemd 自动拉起;
  • ConditionPathExists:没有 Authtoken 时服务不启动。

当前不需要在 ngrok 层做 OAuth 或 Basic Auth,可使用最小策略文件:

1
2
# /etc/ngrok-hermes/traffic-policy.yml
{}

启动并设为开机自启:

1
2
3
4
sudo systemctl daemon-reload
sudo systemctl enable --now ngrok-hermes.service
systemctl is-active ngrok-hermes.service
journalctl -u ngrok-hermes.service -n 50 --no-pager

日志中会出现类似:

1
started tunnel ... "url":"https://<随机子域>.ngrok-free.dev"

该 HTTPS 地址就是手机端要填写的服务器 URL。

6:验收与日常检查

6.1 服务检查

1
2
3
systemctl is-active hermes-web-ui.service
systemctl is-active ngrok-hermes.service
ss -ltnp | grep -E ':(8642|8648)\b'

应满足:

1
2
3
hermes-web-ui.service = active
ngrok-hermes.service   = active
8642 / 8648 都只监听 127.0.0.1

6.2 查看当前公网 URL

ngrok Agent 运行时,在本机 127.0.0.1:4040 提供状态 API:

1
2
curl -sS http://127.0.0.1:4040/api/tunnels \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["tunnels"][0]["public_url"])'

该端口只监听 localhost,不应发布到公网。

6.3 公网与内建认证检查

1
2
3
4
5
# Web UI 静态页面应能加载
curl -I https://<随机子域>.ngrok-free.dev/

# 未带 Hermes Web UI token 时,受保护 API 应返回 401
curl -i https://<随机子域>.ngrok-free.dev/api/hermes/profiles

最终设计中:

  • 首页和静态资源可被 ngrok 公网访问;
  • Web UI API 仍由 Hermes Web UI 内建 Authorization: Bearer <token> 保护;
  • App/浏览器首次连接后,会进入 Hermes Web UI 自己的 token 登录流程。

Hermes Web UI 的内建认证是 token/Bearer 认证,并非单独的 username/password 表单。不要把 Web UI token 发到聊天或提交到仓库。

7:Android Hermes Agent WebUI App 连接

Android App:

1
https://play.google.com/store/apps/details?id=com.hermeswebui.android

配置步骤:

  1. 安装 App 并允许网络访问;

  2. 新增 Server Profile;

  3. 填写 ngrok 返回的 HTTPS URL,例如:

    1
    
    https://<随机子域>.ngrok-free.dev/
    
  4. 连接成功后,在 Hermes Web UI 内输入 token;

  5. 进入聊天页后测试新建会话、发送文本、上传文件等功能。

Android App 是 WebView 容器,并不自带 Hermes 模型、Gateway 或服务器;实际执行仍发生在 WSL 的 Hermes runtime。

8:为什么最终不使用 ngrok OAuth / Basic Auth

理论上,ngrok 可以在外层添加 OAuth 或 HTTP Basic Auth。但与当前 Android Hermes App 和 Hermes Web UI 的组合存在两个重要兼容性问题。

8.1 ngrok OAuth:ERR_NGROK_3303

外层 Google OAuth 的跳转大致为:

1
2
3
4
5
6
Android App WebView
→ ngrok 地址
→ idp.ngrok.com
→ accounts.google.com
→ idp.ngrok.com 回调
→ ngrok 地址

Android App 对“可信 Hermes Host”与外部浏览器/Custom Tab 有导航隔离。OAuth 开始和回调若不在同一 cookie/session 容器中,ngrok 会报:

1
2
ERR_NGROK_3303
URL "state" parameter is invalid

手机 Chrome 中能成功,并不表示 App WebView 一定也能成功。

8.2 ngrok Basic Auth:登录后重复弹框

Basic Auth 使用:

1
Authorization: Basic <base64(username:password)>

而 Hermes Web UI 在登录后调用 API 使用:

1
Authorization: Bearer <Hermes Web UI token>

若对所有 ngrok 路径强制 Basic Auth,则前端进入聊天页后的 /api/... 请求会带 Bearer header;ngrok 不会将它视为 Basic Auth,会返回 401WWW-Authenticate。Android/WebView 会因此持续弹出用户名密码框。

即使只对部分路径加 Basic Auth,也可能被 SPA 路由、后台资源、流式请求或刷新流程触发。为了避免两个认证系统抢占同一个 Authorization header,本次最终选择:

1
2
ngrok:只做 HTTPS 反向隧道
Hermes Web UI:独立承担 token 登录与 API 鉴权

这个选择改善了 Android App 兼容性,但意味着公网首页可被访问。因此 Web UI token 必须保密,且不应把 ngrok URL 广泛传播。

9:Android 麦克风与 STT 设置

9.1 报错:STT settings are required for provider openai

手机 App 点击麦克风后,如果看到:

1
API Error 400: STT settings are required for provider openai

说明:

1
2
3
4
手机已经录到声音并上传到 Web UI
→ Web UI 要调用 OpenAI STT
→ Web UI 自己的 profile 还没有保存 STT provider/model/key 设置
→ 服务端返回 400

这不是 ngrok、Android 麦克风权限或 Hermes Gateway 连接故障。

一个容易忽略的点:Hermes Agent 的 /root/.hermes/config.yaml 中即使已有 stt: 配置,Hermes Web UI 仍维护一套自己的 profile 级 STT 设置。两者不是同一份配置。

9.2 推荐模型:gpt-transcribe

对于 Android App 当前的模式——录完一段音频,再上传文件获取全文——推荐:

1
2
3
4
Provider:OpenAI Whisper
Model:gpt-transcribe
Audio transcoding:发送原始音频
Language:zh(中文为主时可设置)

理由:

  • OpenAI 当前对“已完成录音/文件转写”建议优先使用 gpt-transcribe
  • gpt-live-transcribe 是持续实时音频流、实时字幕/电话场景的模型,不是本 App 当前一次上传一段录音的首选;
  • gpt-4o-transcribe 可作为较高质量的兼容替代;
  • gpt-4o-mini-transcribe 适合希望降低成本、频繁短语音输入的场景;
  • whisper-1 是较旧的兼容兜底,不是当前“最高质量”选择。

在 Web UI 的 STT 设置页中填入 OpenAI 可用 API Key、https://api.openai.com/v1(或实际 OpenAI-compatible 语音转写网关)及上述模型后,保存并执行一次测试转写。

如经常出现量化交易术语、中英混合名称,可增加提示词:

1
中文普通话;量化交易、股票、借券、OEMS、FIX、LMS、SBL、券商、订单、成交、风险控制。

不要把 API Key 写入本文、截图或 Git 仓库。

10:常见故障排查

现象根因处理方式
ERR_NGROK_3200 / endpoint offlinengrok Agent 未运行或启动失败systemctl status ngrok-hermes.service,查看 journal
ERR_NGROK_2237Basic Auth 密码低于 ngrok 最低 8 位要求使用至少 8 位密码;本方案已不使用 Basic Auth
ERR_NGROK_3303ngrok OAuth state cookie 在 WebView/浏览器间丢失不在 Android App 里使用 ngrok OAuth;改用 Web UI 内建 token 或 Tailscale
登录后不断弹 Basic Auth 框ngrok Basic Authorization 与 Hermes Bearer Authorization 冲突移除 ngrok Basic Auth
页面能打开,聊天/API 401Hermes Web UI token 未填写或已失效在 Web UI 登录页重新输入 token,必要时刷新 App 缓存
麦克风报 STT settings are requiredWeb UI 的 profile 级 STT 未配置在 Web UI 的 STT 设置中保存 provider/model/key/base URL
ngrok 日志有短暂 EOF、heartbeat 或 DNS 超时网络短抖动确认 systemd Restart=on-failure,等待 Agent 自动重连;重连后检查 URL
127.0.0.1:8648 connection refusedHermes Web UI 重启中或异常systemctl status hermes-web-ui.service,检查 /root/.hermes-web-ui/logs/server.log

常用排障命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 两个核心服务
systemctl status hermes-web-ui.service --no-pager
systemctl status ngrok-hermes.service --no-pager

# ngrok 隧道与错误日志
journalctl -u ngrok-hermes.service -n 100 --no-pager

# Web UI 本机可用性
curl -I http://127.0.0.1:8648/
ss -ltnp | grep -E ':(8642|8648|4040)\b'

# 不查看 token 内容,只确认 token 文件存在
sudo test -s /root/.hermes-web-ui/.token && echo 'Web UI token exists'

11:安全边界与后续建议

本次方案已经满足“手机 App 远程操作 Hermes”的目标,但风险边界需要明确:

  1. ngrok URL 是公网地址。 不要在公开群、截图或文章中暴露当前实际 URL;
  2. 不要映射 Hermes Gateway 8642、SSH、数据库、KDB、Redis、券商或交易系统端口;
  3. Web UI token 是关键门禁,泄露后应立即更换并重启 Web UI;
  4. 只以低权限 ngrok-agent 账户运行 Agent;
  5. 定期查看 ngrok、Web UI 与 Gateway 日志;
  6. 如果后续只供本人设备使用,迁移到 Tailscale Serve 可进一步缩小公网暴露面;
  7. 若未来需要长期固定域名、多人受控访问,应优先评估 Cloudflare Tunnel + Cloudflare Access,而不是无保护地扩展 ngrok 公网入口。

12:参考资料