查看 Markdown

自托管 Web UI

Paseo 的 daemon 可以自己提供浏览器 Web 应用,地址与它已用于 API 的地址相同。你不需要使用托管在 app.paseo.sh 的应用:让浏览器指向你自己的 daemon,就能获得完整的 UI,连接到你自己的 Agent,运行在你掌控的基础设施上。

以下情况适合这样做:

  • 在你自己的机器或服务器上运行整个 UI。
  • 把它放在你自己的反向代理、HTTPS 或隧道之后。
  • 让整套设置完全自托管,不依赖托管版 Web 应用。

Web 应用随 daemon 软件包一起发布,因此你提供的 UI 总是与 daemon 版本一致。不需要单独构建并保持同步,也不需要处理 UI 与 daemon 的版本偏差。

启用

内置的 Web UI 默认关闭。保存设置,然后启动 daemon:

paseo daemon config set features.webUi.enabled true
paseo daemon start

或者用环境变量运行一个前台部署:

PASEO_WEB_UI_ENABLED=true paseo daemon run

或者把它持久化到 config.json 中,使其在重启后依然生效:

{
  "features": {
    "webUi": {
      "enabled": true
    }
  }
}

然后在浏览器中打开 daemon 的地址:

http://localhost:6767/

如果你的 daemon 不认识 --web-ui,请更新它;这个参数是随内置 Web UI 一起加入的。

连接的工作方式

页面与 daemon 的 API 和 WebSocket 来自同一个源。打开页面时,应用会自动连接回同一个源,因此你通常可以完全跳过“添加主机”步骤:打开 http://localhost:6767/,就能看到你的 Agent。

同一个 HTTP 服务器继续提供 API(/api/*)、MCP(/mcp/*)、服务代理路由和 WebSocket 升级。新增的只有静态文件。要让提供的 UI 指向另一个 daemon,请照常在 UI 中把那个 daemon 添加为主机。

部署拓扑

三种常见的运行方式,按暴露程度从低到高排列:

  • 同一台机器。 daemon 和浏览器在同一台机器上。打开 http://localhost:6767/,无需其他配置。
  • 私有网络(局域网或 VPN)。 从你信任的网络中的其他设备访问 daemon,例如家庭局域网或 Tailscale tailnet。完整的直接连接设置见连接方式指南。
  • 公开的反向代理或隧道。 通过 HTTPS 在一个域名上暴露 UI,在反向代理或隧道处终止 TLS。这是完整的自托管设置。

本页其余部分从本地逐步构建到公开访问。在前面加代理之前,先确认直接连接可以正常工作,这样可以把 daemon 的问题和代理的问题区分开。

在 localhost 之外暴露

daemon 默认监听 127.0.0.1:6767,只能从同一台机器访问。要从其他设备访问,请把它绑定到某个网络接口:

paseo daemon config set daemon.listen 0.0.0.0:6767
paseo daemon start

任何能访问该监听地址的人都可以使用你的 Agent。在绑定到 localhost 之外之前,请设置密码并检查你的主机白名单。中继配对方式让 daemon 保持绑定在 localhost 上,从而完全避免这个问题,见安全。

直接暴露 daemon 时需要配置两件事:

  1. 设置密码,只让获得授权的客户端连接:

    paseo daemon set-password
    

    持久化设置见密码验证。密码验证控制访问,但不加密流量;在任何不受信任的网络上,请在它前面加上 TLS(见下文)。

  2. 允许你的主机名,让 daemon 的 DNS 重绑定防护接受发往你的域名的请求:

    paseo daemon config set daemon.hostnames '[".example.com"]'
    

    主机白名单的工作方式见 DNS 重绑定防护。

Web 应用在身份验证之前加载,这是有意设计的。静态 UI 文件无需 daemon 密码即可提供,以便显示登录界面;API 和 WebSocket 在返回任何 Agent 数据或执行任何命令之前仍然需要密码。不要把“页面加载了”当成“daemon 是开放的”,但在绑定到网络之前一定要设置密码,让页面背后的数据受到保护。

反向代理

要在一个域名上通过 HTTPS 提供 UI,请在反向代理处终止 TLS,并把所有请求转发给 daemon。让 daemon 保持在 localhost 上,只让代理对外暴露。

可用的代理必须:

  • 转发 WebSocket 升级。 应用通过 /ws 上的 WebSocket 流式传输 Agent 输出;不支持升级时,UI 能加载但永远连不上。
  • 不缓冲响应。 终端输出和其他实时流是长连接;缓冲会让 UI 看起来像卡住了。
  • 使用较长的读取超时。 这些流在整个会话期间保持打开。
  • 允许较大的请求体。 提示词和文件上传可能很大。
  • 保留 Host 请求头并传递 X-Forwarded-Proto。 daemon 用它们告诉应用应连接回哪个源以及使用哪种协议(wss:// 还是 ws://)。丢掉它们,自动连接就会指向错误的地方。

Nginx

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 443 ssl;
  server_name paseo.example.com;

  ssl_certificate     /etc/letsencrypt/live/paseo.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/paseo.example.com/privkey.pem;

  client_max_body_size 100m;

  location / {
    proxy_pass http://127.0.0.1:6767;
    proxy_http_version 1.1;

    # WebSocket upgrade
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # Preserve origin + scheme so the UI connects back over wss://
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # Long-lived, unbuffered streams
    proxy_buffering off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
  }
}

Caddy

Caddy 会为你处理 TLS、WebSocket 升级、请求头转发和流式传输:

paseo.example.com {
  reverse_proxy 127.0.0.1:6767
}

这就是全部配置。Caddy 会自动申请证书,并默认保留 Host 和 X-Forwarded-Proto。

HTTPS 与 TLS

在代理(或隧道)处终止 TLS,并通过 localhost 上的普通 HTTP 转发给 daemon,上面的配置就是这样做的。当页面通过 HTTPS 提供且代理传递 X-Forwarded-Proto: https 时,应用会自动通过 wss:// 连接回来。你不需要在任何地方配置协议;它跟随边缘节点。

daemon 默认信任来自回环地址代理的转发请求头,上面所有的设置都是如此:代理或隧道转发到 127.0.0.1:6767。

如果你的代理从其他地址访问 daemon,例如在某些 Docker、局域网或负载均衡器设置中,请配置受信任的代理范围:

{
  "daemon": {
    "trustedProxies": ["loopback", "172.16.0.0/12"]
  }
}

PASEO_TRUSTED_PROXIES 接受相同的逗号分隔值:

PASEO_TRUSTED_PROXIES=loopback,172.16.0.0/12 PASEO_WEB_UI_ENABLED=true paseo daemon run

只有当你最后一层受信任的代理会覆盖客户端提供的 X-Forwarded-* 请求头时,才使用 trustedProxies: true。否则客户端可以伪造转发请求头的值。

如果你通过 HTTPS 提供 UI,但应用尝试通过 ws:// 连接(浏览器会把它当作混合内容拦截),说明你的代理没有转发 X-Forwarded-Proto,或者 daemon 不信任代理的地址。修复相应的那一项。

对于远程或中继方式(通过 Paseo 中继而不是反向代理驱动 daemon),中继有它自己的公开与内部 TLS 设置,见安全。

隧道

如果你不想管理反向代理或开放端口,隧道可以提供一个转发到本地 daemon 的 HTTPS URL。

  • Tailscale Serve 把它保留在你的 tailnet 内,不公开暴露,并为你处理 TLS:

    tailscale serve --bg http://127.0.0.1:6767
    

    通过 https://<your-machine>.<tailnet>.ts.net/ 访问。只有你的 tailnet 中的设备可以连接。

  • Cloudflare Tunnel 通过一个支持 TLS 和 WebSocket 的公开主机名暴露它:

    cloudflared tunnel --url http://localhost:6767
    

    Cloudflare 会终止 TLS 并设置 X-Forwarded-Proto: https,因此自动连接可以正常工作。由于 URL 是公开的,请设置 daemon 密码。

安全

自托管 Web UI 意味着由你决定谁能访问 daemon。要点如下:

  • 在绑定到 localhost 之外之前设置密码。 静态页面无需密码即可加载,但 Agent 数据和命令都在密码之后。见安全。
  • 在任何不受信任的网络前加上 TLS。 密码验证保护的是访问,而不是机密性。
  • 尽可能让 daemon 保持在 localhost 上,只让反向代理或隧道对外暴露。
  • 在自定义域名上提供服务时,检查你的主机白名单。

完整的威胁模型、中继加密和 DNS 重绑定细节,见安全和 SECURITY.md。

故障排查

  • / 显示空白页或 404。 Web UI 没有启用。用 --web-ui 启动 daemon,并用 paseo daemon status 确认你访问的就是这个 daemon。
  • 页面加载了但一直连不上。 代理没有转发 WebSocket 升级,或者剥离了 Host 请求头。检查代理配置中的升级请求头。
  • 连上之后输出卡住。 开启了响应缓冲,或者读取超时太短。关闭缓冲并提高超时时间。
  • HTTPS 下出现“Mixed content”或连接被拦截。 应用退回到了 ws://。要么代理没有发送 X-Forwarded-Proto: https,要么 daemon 不信任代理的地址。转发该请求头;如果代理不在回环地址上,再配置 daemon.trustedProxies。
  • 403 Invalid Host header。 你的域名不在白名单中。用 --hostnames 或 daemon.hostnames 添加它,见 DNS 重绑定防护。
  • 较大的提示词或上传失败。 调高代理的最大请求体大小(Nginx 中的 client_max_body_size)。

另请参阅

  • 安全:连接方式、中继加密、密码验证、主机白名单。
  • 配置:config.json、环境变量和 CLI 覆盖项。
  • CLI:paseo daemon 命令。
  • 社区项目:社区构建的自托管工具。

赞助商