预览
预览会把一个带网页界面的东西放到公开链接上:一个监听中继机器上某个端口的开发服务器,或一个装普通文件的文件夹。你可以从任何设备打开该链接,也可以把它发给任何人。中继不会被暴露:预览由一个单独的监听器在单独的主机名上提供。
典型用途:
- 在手机上打开应用。 智能体启动开发服务器,你轻点链接,在真实设备上试用布局。
- 把链接发给别人。 同事无需访问你的机器或中继,就能打开一个原型或已构建的网站。
- 把 webhook 指向开发服务器。 需要公开地址的服务可以调用你正在构建的应用。
本页是给所有者和运维人员的指南。智能体的工具及其参数、结果和限制,在 preview 中有描述。
预览链接是公开的
链接中的令牌(128 位)是唯一的保护。任何持有该链接的人无需登录就能访问该应用。对含有真实数据的内容做预览,就是真正把它暴露出去。用完预览后请关闭它。
预览如何与中继分开 #
预览由第二个 HTTP 监听器在它自己的端口上提供,并在它自己的主机名上发布。由此得出两点:
- 中继不在那个端口上。 它的 API、网页应用、实时连接和分享链接都没有挂载在那里。预览流量无法到达它们,因为它们并不存在于那个监听器上。创建或关闭预览需要中继的访问令牌,并且只发生在中继自己的端口上。
- 主机名不同。 被预览的应用运行在与中继不同的另一个浏览器源中。它的 JavaScript 无法发送你的中继登录 cookie,也无法读取中继网页应用的存储。
主机名必须不同,而不仅仅是端口不同。cookie 会忽略端口,因此 relay. 会共享 relay. 的登录 cookie。当主机名相同时,中继会拒绝启动该通道。
除此之外,该通道绝不会把中继的登录 cookie(copilot_)或携带中继访问令牌的 Authorization 头转发给被预览的应用,即使手工构造的请求携带它们。应用自己的 Authorization 头会照常通过。
预览彼此之间确实共享一个主机名,每个都在自己的路径下。参见需要记住的几点。
设置 #
在运维人员设置之前,预览是关闭的。共有三个步骤。
1. 一个主机名 #
挑选一个你控制的、名称上与中继不同的主机名,例如 preview. 与 relay. 并列。把它的 DNS 指向你的前置代理或隧道。预览按路径而非子域划分命名空间,因此在你已有的区域下一个单标签主机名就足够了。在 Cloudflare 下,该名称由 Universal 证书覆盖。像 *. 这样的两标签通配符则不会,这就是为什么没有为每个预览分配子域。
2. 一个前置代理 #
代理会把发往预览主机名的请求转发给预览监听器,后者默认使用中继端口加一(当中继使用 3333 时是 3334)。使用你通过 previews. 固定的端口;代理需要一个稳定的目标。
Caddy
preview.example.com {
reverse_proxy 127.0.0.1:3334
}
Cloudflare Tunnel
在你已经使用的隧道上添加第二个公开主机名,可以在 Cloudflare 控制台的 Public Hostnames → Add(公共主机名 → 添加)下添加,也可以写进隧道的 config.:
ingress:
- hostname: preview.example.com
service: http://127.0.0.1:3334
- hostname: relay.example.com
service: http://127.0.0.1:3333
- service: http_status:404
3. previews 配置块 #
把该配置块添加到中继的 config. 并重启中继(参见 oar restart):
{
"publicHostnames": ["relay.example.com"],
"previews": {
"enabled": true,
"port": 3334,
"bindHost": "127.0.0.1",
"publicBaseUrl": "https://preview.example.com",
"allowedTargetHosts": [],
"maxLive": 8
}
}
| 键 | 那里放什么 |
|---|---|
public |
中继自身应答的每个主机名。针对 public 的检查会使用这个列表(并且总是包括 localhost 和 127.),因此你漏掉的主机名不会被检查。 |
previews. |
设为 true 以开启该通道。默认是关闭的。 |
previews. |
监听器的端口。默认:中继端口加一。0 会在每次启动时挑选一个空闲端口,前置代理无法跟上它。 |
previews. |
监听器绑定到哪里。默认 127.。让它保持环回:前置代理运行在同一台机器上。 |
previews. |
只有当 bind 不是环回时才需要。那样每个预览就能不经代理直接从网络访问。 |
previews. |
该通道的公开地址:https://。链接由它构建。 |
previews. |
除本机之外,端口预览可以指向的主机,例如容器或虚拟机。 |
previews. |
整个中继同时最多有多少预览。默认 8。 |
每个键、默认值和边界情况都在 config.json 的表格中。
环境变量覆盖 #
COPILOT_、COPILOT_、COPILOT_、COPILOT_ 和 COPILOT_ 会覆盖相应的键。它们的值和谁覆盖谁,见环境变量。
当通道拒绝启动时 #
配置错误的通道绝不会拖垮中继。中继会继续运行,通道保持关闭。这些检查都不会变成警告,因为每一项都在保护上面描述的隔离。
| 原因 | 消息 |
|---|---|
public 缺失或不是 http(s) 地址 |
previews. |
public 使用了中继的主机名 |
previews. |
| 端口低于 1024 | previews. |
| 端口是中继的端口或其 CLI 端口 | previews. |
| 无法确定端口 | previews. |
bind 不是环回 |
previews. |
你会在两个地方看到它:
- 中继日志,在启动时:每项失败的检查一行
PREVIEW LANE disabled: <message>。当检查通过时,日志会显示[preview] listener on http://。日志在哪里:文件与文件夹。127. 0. 0. 1: 3334 → https:// preview. example. com - 发布请求的应答。 API 会以 503 应答,并带上
Preview lane failed its startup checks和details中的消息。智能体的preview工具会转达这一点,并让智能体告诉你。当没有设置previews.时,应答是enabled Preview lane is disabled (set previews.,日志中没有那一行。enabled and previews. public Base Url)
Settings → Previews(设置 → 预览)和 / 的提示不会显示原因。设置选项卡会列出存在的预览,而提示只会说发布失败(参见 /)。请查看中继日志。
如果端口被另一个程序占用,检查会通过,但监听器无法绑定。日志随后会显示 PREVIEW LANE failed to bind :<port> — <reason>。就 API 而言,通道是启动的,但预览端口上没有任何应答,因此链接打不开。释放该端口,或设置另一个 previews.。
三种发布方式 #
让智能体来做 #
说出你想要什么:“启动开发服务器,给我一个链接”“给我看看构建好的网站”。智能体会使用 preview 工具,或在某个提供商没有该工具时使用中继的预览 API。它会告诉你链接,以及该链接是公开的。哪些提供商有该工具,见参考页上的表格。
输入区中的 /preview #
该命令在你的浏览器中针对中继运行。它适用于每个提供商,且不消耗智能体的轮次。当消息没有附加任何内容时,它就会运行。提示会出现在对话中。
| 你输入 | 会发生什么 |
|---|---|
/ |
以标签“web app”发布端口 5173。打印 Preview published: <url>。 |
/ |
把文件夹 ./ 作为静态预览发布,标签为“built site”。第一个词是文件夹,因此它不能包含空格。打印 Preview published: <url>。 |
/ |
列出中继的每个预览,每行一个:令牌的前 8 个字符、标签、端口或文件夹、如果离线则是 — offline,以及链接。如果没有,则打印 No live previews.;当它关闭时,打印 The preview lane is disabled on this relay.。 |
/ |
关闭此对话的预览。没有时,打印 No live previews in this conversation.。有多个时,打印匹配项并要求更长的前缀。 |
/ |
关闭令牌以这 4 到 32 个十六进制字符开头的预览,可在任何对话中。打印 Preview closed: <label>。没有匹配时:No preview matches "3f9c". |
/ |
打印用法行,任何它读不懂的输入也一样。 |
由数字组成的第一个词总是一个端口,超出 1 到 65535 的端口会打印用法行。对于名为 8080 的文件夹,请输入 ./。
如果发布失败,无论什么原因(端口错误、文件夹在工作目录之外、通道已满、通道被禁用),提示都是一样的:
Preview failed — check the port/path, or whether the preview lane is enabled (Settings → Live previews).
确切原因不会显示。列表在 Settings → Previews 上,位于标题 Live previews(实时预览)下。要查看原因,请调用 API(下文),或查看疑难解答中的表格。如果无法访问中继,提示是 Preview command failed — could not reach the relay. 命令菜单及其按键见输入区。
API #
上面的一切都使用中继端口上的同一个 API。它需要中继的访问令牌(config. 中的 auth,也就是你登录所用的同一个令牌)作为 bearer 令牌。请先注册,这样你就知道基础路径,然后在它下面启动开发服务器:
curl -sX POST http://127.0.0.1:3333/api/previews \
-H "authorization: Bearer $RELAY_TOKEN" -H 'content-type: application/json' \
-d '{"conversationId":"<conversation id>","port":5173,"label":"web app"}'
{
"ok": true,
"url": "https://preview.example.com/test_0123456789abcdef0123456789abcdef/",
"basePath": "/test_0123456789abcdef0123456789abcdef/",
"preview": { "token": "0123456789abcdef0123456789abcdef" },
"existing": false
}
当对话已经有一个针对同一端口或文件夹的实时预览、并且你取回的是那一个时,existing 为 true。
| 请求 | 用途 |
|---|---|
POST / |
port 或 dir(不能两者都有),可选 conversation、host、label。 |
GET / |
所有预览。?conversation,或会话 worker 发送的对话头,会把它缩小到一个对话。 |
DELETE / |
关闭一个。开发服务器不受影响。0.9.14 新增 像 GET 那样指定对话的请求,只能关闭该对话的预览;其他对话的会被拒绝(403)。 |
让应用在预览路径下工作 #
在共享的预览主机名上,每个预览都位于 / 下。由代理的行为得出的规则是:
- 浏览器请求
/。test_<token>/ … - 应用收到的是不含该前缀的路径。
- 因此应用必须在其页面中放入要么带前缀、要么是相对地址的链接和资源 URL,并且它还必须应答不带前缀的请求。
代理在途中会做什么:
| 代理 | |
|---|---|
| 去掉前缀 | 应用收到 / 之后的路径,带查询参数。访问裸的 / 会被重定向(301)到带尾斜杠的同一地址,因此相对 URL 会在预览内解析。 |
发送 X- |
不带尾斜杠的前缀。 |
发送 X- |
预览监听器收到的 Host。 |
发送 X- |
前置代理发送的那个;如果它没发送,则是 https。 |
发送 X- |
连接到监听器的一方的地址。在同一台机器上的前置代理之后,那就是代理。 |
设置 Host |
为应用自己的地址(127.),供会检查它的开发服务器使用。 |
重写 Location |
到 / 的重定向会变成 /,到应用自己地址的绝对重定向也一样。已经带前缀的重定向、以 // 开头的重定向、相对重定向或指向其他站点的重定向则保持不变。 |
限定 Set- |
cookie 会得到 Path=/ 并去掉 Domain,这样某个预览的 cookie 不会到达另一个预览。名为 copilot_ 的 cookie 会被丢弃。 |
| 标记响应 | X-,并且它会丢弃 Service-。 |
它不会重写页面、样式表或脚本的主体。一个把 / 写进其 HTML 的应用,请求的是预览主机名的根,那里没有预览,浏览器会得到 No such preview 页面(404)。这表明该应用不知道自己的前缀。
哪些能直接工作 #
- 相对 URL。 页面使用
./这类地址的应用能直接工作:页面位于assets/ app. js /,因此它们会在预览内解析。这由代理的工作方式推出;并未按框架逐一测试。test_<token>/ - 读取前缀。 在不带前缀的路径上路由、并从
X-构建链接的应用,正好符合该规则。Forwarded- Prefix - 构建好的网站。 用相对路径或等于前缀的 base 构建,然后把输出文件夹发布为静态预览。对
/的请求会从该文件夹提供,因此两种 URL 都能工作。test_<token>/ assets/ …
哪些不能直接工作 #
一种让应用只应答带前缀请求的工具设置不适合,因为代理会把它去掉。
- 把
base设为前缀的 Vite 开发服务器(--base)。 已对照中继机器上的 Vite 8.3.1 源代码检查:该开发服务器只处理以base开头的路径。它对/的请求会以到base的重定向应答,而代理对此保持不动,因此浏览器会循环。其他不带前缀的请求会得到 404,说该服务器有一个公开的基础 URL。热重载只在base下的路径监听,被去掉前缀的请求无法匹配。请改用构建好的网站方式,或用一个从根路径以相对 URL 提供服务的 Vite 设置。 - 把路由挂载在前缀处的 Express(
app.)。应用收到的是use(prefix, router) /,而不是x /,因此挂载无法匹配。把路由挂载在test_<token>/ x /并从X-构建链接。Forwarded- Prefix
Next.js(base、asset)和 SvelteKit(kit.)在 OAR 自己给智能体的说明中被提及,但这些说明并未对照代理的行为检查,而且这里没有这两种工具可供测试。如果它们的 base 设置让应用期望每个请求都带前缀,那它就和 Vite 一样不适合。未经验证:请在依赖之前先试一下。
当应用需要前缀时,先发布
智能体在启动开发服务器之前会先调用 create,这样应用在需要前缀时就能拿到它。对于用相对 URL 工作的应用,顺序无所谓。
静态预览 #
用 dir 而不是 port,中继会自己提供一个文件夹。不需要运行任何东西,这适合构建输出、原型、单个 HTML 文件或覆盖率报告。
- 该文件夹取自对话的工作目录(绝对路径,或相对于它)并且必须位于其中。没有对话时发布的预览会使用中继自己的工作目录。这会在你发布时以及每次请求时检查,因此之后创建的符号链接无法指向外面。
- 对于文件夹路径,会提供
index.。没有任何缓存。html - 绝不提供:名称以点开头的文件和文件夹(
.、git .)以及密钥文件(env *.、pem *.,以key id_、rsa id_、ed25519 id_或ecdsa id_开头的名称)。dsa - 相对资源路径能工作。以预览前缀开头的也能工作。根绝对路径(
/)不能。assets/ app. js - 没有
index.的文件夹,以及任何缺失的文件,会得到html Not found页面(404)。 - 静态预览始终是
online,并拒绝 WebSocket 连接。
管理预览 #
- 后台任务面板中的卡片。 对话的每个预览都有一张卡片,显示其标签、端口、一条说明链接是公开的提示、中继检查过后的
online或offline徽标,以及 Open(打开)、Copy link(复制链接)和 Close(关闭)按钮。该面板在后台任务中有描述。 - 对话记录中的链接卡片。 当预览在对话的一轮运行期间发布时,回复会带一张链接卡片,无论是智能体还是你用
/发布的。在轮次之间发布的预览没有对话记录卡片。一旦预览消失,卡片会显示一个preview closed徽标而没有链接。分享的对话只会显示带标签的卡片,其文本中引用的每个预览链接都会显示为[preview link removed]。 - Settings → Previews。 Live previews 列表会显示中继的每个预览、来自所有对话,带对话 id 的前 8 个字符,以及 0.9.11 新增 它何时被发布和上次打开。当你离开它的对话后,可以在这里关闭它。参见 Settings(设置)。
offline 和 closed 是不同的。中继每 15 秒检查一次每个端口预览,方式是尝试连接到该端口(并在你发布时立刻检查一次)。当没有任何应答时,卡片会显示一个 offline 徽标和说明“offline — dev server not responding”。链接仍然有效:在同一端口再次启动服务器,同一个链接就能工作。closed 意味着预览已消失,链接不再解析。
关闭预览只会结束该链接。开发服务器会继续作为它本来的后台任务运行。
预览不会自行过期。它会在你关闭它时结束,或者 0.9.11 新增 在你删除其对话时结束(包括通过重建中继缓存而删除)。归档对话后,它的预览仍然有效,并显示在 Settings → Previews 中。
在同一个对话中再次发布同一端口或文件夹,会返回已经生效的那个预览,链接相同、标签不变,而不是一个新的。
整个中继同时最多有 8 个预览生效(previews.)。拒绝消息会列出最早的三个预览,包括各自的发布时间和上次打开时间,以及如何关闭它们。
中继重启之后 0.9.11 新增 #
预览在中继重启、更新或崩溃后依然存在:中继把它们保存在其数据库中,并在启动时恢复它们,因此你发出去的链接会继续工作。每个预览都会先被再次检查,当检查发现它不再有效时就结束:
- 一个文件夹预览,其文件夹已消失,或不再位于其对话的工作目录内;
- 一个端口预览,在机器重启之后:开发服务器已消失,该端口稍后可能属于别的东西;
- 一个其对话已不存在的预览。
Linux 和 macOS 会可靠地报告重启。在 Windows 上,Restart(重启)算作一次重启;但在 fast startup(快速启动)开启时(这是 Windows 的默认设置),Shut down(关机)后再开机则不算:之前的端口预览会恢复,并连到届时监听该端口的任何程序。请关闭你不再需要的预览。
由于数据库忙、或文件夹因为消失之外的原因无法读取,而无法应答的检查,不会结束该预览。在此期间它不会被提供:Settings → Previews 和工具的 list 会把它显示为未提供(unverified),它会计入上限,也可以被关闭。中继每 15 秒再次检查它,一旦检查有应答,就会在其链接下再次提供它;在其对话中再次发布同一端口或文件夹会立即做到这一点。十分钟没有应答后,该预览会结束,日志会说明这一点。
端口预览在启动后也会立刻探测一次,因此它的 online 或 offline 徽标是最新的。中继的日志用短哈希来命名预览,从不用它的令牌。另见重启保留什么、丢失什么。
WebSocket #
端口预览会在同一前缀下透传 WebSocket 连接,前缀像任何请求一样被去掉,因此当浏览器通过预览链接连接时,应用自己的 socket 能工作。(热重载是应用自己的事:参见上面的 Vite 说明。)静态预览,以及未知或已关闭的链接,会用 404 应答 WebSocket 请求。如果应用以普通响应应答升级,浏览器会得到那个状态。如果应用挂了,则是 502。
需要记住的几点 #
- 预览彼此共享一个源。 cookie 被限定到每个预览的路径,令牌也无法被猜到,但两个预览彼此之间的隔离,不像每个预览与中继之间那样彻底。
- 默认只能指向你自己的机器。 端口预览只能指向环回地址,加上
previews.中的主机。中继自己的端口总是被拒绝,因为发布它会把自己的登录页放到公开主机名上。allowed Target Hosts - 中继不会运行该应用。 它绝不会启动或重启开发服务器。如果服务器停止,卡片就会变成
offline。
按提供商 #
预览在每个提供商上看起来都一样;只有智能体发布的方式不同,正如参考页所示。/ 和 Settings → Previews 对它们全部适用。
疑难解答 #
| 你看到什么 | 原因和解决办法 |
|---|---|
智能体或 API 以 Preview lane is disabled 或 Preview lane failed its startup checks(503)应答 |
通道关闭了,或拒绝启动。阅读中继日志中的 PREVIEW LANE disabled 行,修正 previews,重启中继。参见当通道拒绝启动时。 |
链接显示 Preview upstream unreachable 页面(502) |
端口上没有监听,或连接失败。在同一端口再次启动开发服务器并重新加载;链接仍然有效。 |
资源缺失或页面空白,且对 / 的请求得到 No such preview(404) |
应用写出了根绝对路径。让它生成相对或带前缀的 URL(规则)。 |
| 页面循环加载,或应用以 404 应答并提到“public base URL” | 应用期望每个请求都带前缀,但代理把它去掉了(哪些不能直接工作)。 |
整个链接都是 No such preview |
链接未知或已关闭,其对话被删除,或重启后发现它不再有效(中继重启之后)。请重新发布。 |
预览显示为未提供(unverified) |
中继在启动时无法检查它。它每 15 秒会再次检查;在其对话中再次发布同一目标会立刻把它带回来。 |
| 链接完全打不开 | 前置代理或 DNS 未设置,或监听器无法绑定其端口(日志中的 PREVIEW LANE failed to bind)。 |
卡片显示 offline |
开发服务器已停止应答。在同一端口再次启动它。 |
Too many live previews (max 8); previews stay until closed. Oldest: …(429) |
在 Settings → Previews 中、用 /、或用 DELETE / 关闭一个;智能体可以用工具的 close 操作关闭它自己对话的;或提高 previews.。 |
Port <n> belongs to the relay and cannot be previewed(400) |
你给了中继的端口、它的 CLI 端口或预览端口。请发布开发服务器的端口。 |
Target port must be between 1024 and 65535(400) |
使用更高的端口。 |
Target host ... is not loopback and not in previews.(400) |
把该主机添加到 previews.。 |
dir must live inside the workspace root (...、Directory not found: ... 或 Not a directory: ...(400) |
文件夹必须存在,并且位于对话的工作目录内。 |