Files
paseo/public-docs/web-ui.zh-CN.md
Mohamed Boudra bd15e40e94 Serve paseo.sh in Simplified Chinese (#7373)
* feat(website): serve paseo.sh in Simplified Chinese

Every page of the website now has a Simplified Chinese version under
/zh-CN: the interface, docs, blog posts, comparison pages, legal pages,
all changelog releases and the homepage mockup. English URLs are unchanged.

The URL alone decides the language. There is no browser-language redirect
and no stored preference, because either would have to redirect the
unprefixed English URLs that people share and search engines index.
Internal links carry the page's language instead, and hreflang alternates
point Chinese search results at /zh-CN.

Translated headings pin the English anchor with {#id}, so section links
work in both languages. content-translations.test.ts fails when a page
has no translation or a translation's headings drift from the English.
Changelog releases and plugin listings without a translation render
whole in English under a notice, so a release never waits on one.

Plugin manifests and the registry gain optional translations and
OVERVIEW.zh-CN.md fields; older clients and registries ignore them.

* feat(website): replace the native language select with an EN/ZH menu

The header showed an OS-native select whose popup did not match the site.
The trigger now shows the language abbreviated (EN, ZH) and opens a menu in
the app's dropdown style that names each language in itself, with its name
in the page language underneath. Rows are real links to the same page in
that language; arrows, Escape, outside presses and taps behave as a menu
button, and the menu keeps itself on screen when a narrow header pushes the
trigger toward an edge.

Also drops the stray space after the full-width comma in the Chinese
homepage subtitle.

* docs(website): translate main's new docs and 0.12.0 release notes

Main added Keep run workspaces to the schedules docs, rewrote plugin icons
around inline SVG, and promoted 0.12.0 with new entries. Bring the Chinese
pages and changelog in line so no section falls back to English.

* fix(server): leave plugin listing translations to the registry

The daemon rejected manifests whose translations the registry would refuse,
so a plugin main installs could fail to load here. Paseo never reads
translations; the registry validates them at publish. Docs now say which
checks are automated at publish and which are human review.

* docs(plugins): describe the registry's final overview translation discovery

* style(server): format manifest test

* test(app): measure sidebar rows one at a time

After a reload the developer error toast appears while expectVerticalOrder
measures every row at once. Each concurrent boundingBox runs the toast
locator handler, and only one of them resumes, so the poll timed out on
every attempt since #7424. Measuring sequentially keeps one action in flight.

* test(app): return sidebar row measuring to main

#7480 hides the developer toast instead of running a locator handler, so
measuring the rows concurrently no longer races it. Reverts 41ac23016.

* test(app): wait for the compact shell before opening compact Changes

The shell survives the breakpoint, so right after the resize the wide
Explorer could still be visible: the test skipped the Explorer toggle and
the tab then disappeared, or toggled at the wrong time. Wait for the
compact-shell marker, as the composer helpers do since #7424.

* docs(website): translate main's plugin file panel docs

* test(app): edit the code font size after the font family remount

A committed appearance change remounts the settings screen through
AppearanceStyleBoundary, and the new form starts from the stored size.
Filling the size before that remount lost the edit in CI.

* docs(website): translate main's client/ui import docs

Follows #7474: one /client/ui entry, StreamedText instead of useRevealedText,
SettingsGroup dropped from the settings table, and the new
'UI imports in v0.13' migration section.

* test(app): wait for the previous layout's file panel picker to leave

After a resize the old layout's picker animates out while the new one is
already mounted, so the Use button matched two elements and the strict
locator failed. Wait for the shell's layout and a single picker first.
2026-10-21 13:13:03 +07:00

11 KiB
Raw Permalink Blame History

title, description, nav
title description nav
自托管 Web UI 从你自己的 daemon 提供 Paseo Web 应用,并通过你自己的局域网、VPN、反向代理或隧道访问它。 Web UI

自托管 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 命令。
  • 社区项目:社区构建的自托管工具。