所有頁面
本頁內容

預覽

預覽會把具有網頁介面的東西放到公開連結上:在中繼所在機器的某個連接埠上監聽的 dev server,或是一個存放一般檔案的資料夾。你可以從任何裝置開啟這個連結,也可以把它傳給任何人。中繼本身不會因此暴露:預覽由另一個監聽器在另一個主機名稱上提供。

常見用途:

  • 在手機上開啟 App。 agent 啟動 dev server,你點一下連結,就能在實際裝置上試用版面。
  • 把連結傳給別人。 同事不需要存取你的機器或中繼,就能開啟設計稿或建置好的網站。
  • 讓 webhook 指向 dev server。 需要公開位址的服務可以呼叫你正在開發的 App。

本頁是給擁有者和管理者的指南。agent 的工具及其參數、結果與限制,請見 preview。

預覽連結是公開的

連結中的 token(128 位元)是唯一的保護。任何拿到連結的人都不必登入就能連到這個 App。為含有真實資料的東西建立預覽,就是真正的暴露。用完後請關閉預覽。

預覽如何與中繼隔開 #

預覽由第二個 HTTP 監聽器在它自己的連接埠上提供,並發布在它專屬的主機名稱上。這帶來兩點:

  • 中繼不在那個連接埠上。 中繼的 API、網頁 App、即時連線和分享連結都沒有掛載在那裡。預覽流量無法觸及它們,因為它們在那個監聽器上根本不存在。建立或關閉預覽需要中繼的存取 token,而且只能在中繼自己的連接埠上進行。
  • 主機名稱不同。 被預覽的 App 和中繼在不同的瀏覽器來源中執行。它的 JavaScript 無法送出你的中繼登入 cookie,也無法讀取中繼網頁 App 的儲存空間。

必須是主機名稱不同,而不只是連接埠不同。cookie 會忽略連接埠,所以 relay.example.com:3334 會共用 relay.example.com 的登入 cookie。主機名稱相同時,中繼會拒絕啟動預覽通道。

此外,預覽通道絕不會把中繼的登入 cookie(copilot_auth)或含有中繼存取 token 的 Authorization 標頭轉送給被預覽的 App,即使手動建構的請求帶有它們也一樣。App 自己的 Authorization 標頭則會照常通過。

不過,預覽之間共用同一個主機名稱,各自位於自己的路徑下。見注意事項。

設定 #

在管理者設定之前,預覽是關閉的。共有三個步驟。

1. 主機名稱 #

選一個你能控制、名稱與中繼不同的主機名稱,例如在 relay.example.com 旁邊用 preview.example.com。把它的 DNS 指向你的前端 proxy 或 tunnel。預覽是以路徑而不是子網域區分,所以在你已有的 DNS 區域下用一個單層的主機名稱就夠了。在 Cloudflare 上,這個名稱由 Universal 憑證涵蓋。像 *.preview.example.com 這種兩層的萬用字元則不在涵蓋範圍內,這也是為什麼不為每個預覽分配子網域。

2. 前端 proxy #

proxy 會把預覽主機名稱送到預覽監聽器,監聽器預設使用中繼的連接埠加一(中繼使用 3333 時為 3334)。請用 previews.port 固定一個連接埠;proxy 需要穩定的目標。

Caddy

preview.example.com {
    reverse_proxy 127.0.0.1:3334
}

Cloudflare Tunnel

在你已經使用的 tunnel 上新增第二個公開主機名稱,位置在 Cloudflare 儀表板的 Public Hostnames → Add(公用主機名稱 → 新增)下,或在 tunnel 的 config.yml 中:

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.json,然後重新啟動中繼(見 oar restart):

{
  "publicHostnames": ["relay.example.com"],
  "previews": {
    "enabled": true,
    "port": 3334,
    "bindHost": "127.0.0.1",
    "publicBaseUrl": "https://preview.example.com",
    "allowedTargetHosts": [],
    "maxLive": 8
  }
}
設定鍵 要填什麼
publicHostnames 中繼本身回應的每一個主機名稱。對 publicBaseUrl 的檢查會使用這份清單(並一律包含 localhost 和 127.0.0.1),所以你漏掉的主機名稱不會被檢查。
previews.enabled 設為 true 以開啟預覽通道。預設為關閉。
previews.port 監聽器的連接埠。預設:中繼的連接埠加一。0 會在每次啟動時挑一個空閒的連接埠,前端 proxy 無法跟著變動。
previews.bindHost 監聽器綁定的位址。預設為 127.0.0.1。請保持在迴路位址上:前端 proxy 在同一台機器上執行。
previews.allowPublicBind 只有在 bindHost 不是迴路位址時才需要。此時每個預覽都能不經 proxy 從網路上連到。
previews.publicBaseUrl 預覽通道的公開位址:https://preview.example.com。連結會以它建構。
previews.allowedTargetHosts 除了這台機器之外,連接埠預覽可以指向的主機,例如容器或 VM。
previews.maxLive 整個中繼同時存在的預覽數上限。預設為 8。

所有設定鍵、預設值與邊界情況,都列在 config.json 的表格中。

環境變數覆寫 #

COPILOT_PREVIEWS_ENABLED、COPILOT_PREVIEWS_PORT、COPILOT_PREVIEWS_BIND_HOST、COPILOT_PREVIEWS_PUBLIC_BASE_URL 和 COPILOT_PREVIEWS_ALLOWED_TARGET_HOSTS 會覆寫對應的設定鍵。它們的值以及誰優先於誰,請見環境變數。

預覽通道拒絕啟動時 #

設定錯誤的預覽通道絕不會讓中繼停擺。中繼會繼續執行,而預覽通道保持關閉。這些檢查沒有一項會降級為警告,因為每一項都在保護上述的隔離。

原因 訊息
publicBaseUrl 缺少,或不是 http(s) 位址 previews.publicBaseUrl is required (an http(s) URL on a hostname of your own)
publicBaseUrl 使用了中繼的主機名稱 previews.publicBaseUrl must use a different hostname than the relay (got <hostname>)
連接埠低於 1024 previews.port must be >= 1024 (got <port>)
連接埠是中繼的連接埠或它的 CLI 連接埠 previews.port <port> collides with a relay port
無法算出連接埠 previews.port could not be resolved (set previews.port or config.port)
bindHost 不是迴路位址 previews.bindHost <address> is not loopback; set previews.allowPublicBind to override

你會在兩個地方看到:

  • 中繼記錄,在啟動時:每一項未通過的檢查會有一行 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 中列出訊息。agent 的 preview 工具會轉達這個回應,而 agent 會被告知要轉告你。未設定 previews.enabled 時,回應為 Preview lane is disabled (set previews.enabled and previews.publicBaseUrl),記錄中也不會有任何一行。

Settings → Previews(設定 → 預覽)和 /preview 的通知都不會顯示原因。設定分頁會列出現有的預覽,通知只說發布失敗(見 /preview)。請查看中繼記錄。

如果連接埠被其他程式佔用,檢查會通過,但監聽器無法綁定。記錄會顯示 PREVIEW LANE failed to bind :<port> — <reason>。就 API 而言預覽通道已啟動,但預覽連接埠上沒有任何東西回應,所以連結打不開。請釋放該連接埠,或設定另一個 previews.port。

三種發布方式 #

請 agent 發布 #

說出你要什麼:「啟動 dev server 並給我一個連結」、「讓我看看建置好的網站」。agent 會使用 preview 工具;在沒有這個工具的供應商上,則使用中繼的預覽 API。它會告訴你連結,並說明連結是公開的。哪些供應商有這個工具,請見參考頁面上的表格。

輸入區中的 /preview #

這個指令在你的瀏覽器中對中繼執行。它適用於所有供應商,而且不耗用 agent 的回合。只有在訊息沒有附加任何東西時才會執行。通知會出現在對話中。

你輸入 會發生什麼
/preview 5173 web app 發布連接埠 5173,標籤為「web app」。顯示 Preview published: <url>。
/preview ./dist built site 把資料夾 ./dist 發布為靜態預覽,標籤為「built site」。第一個字是資料夾,所以它不能包含空格。顯示 Preview published: <url>。
/preview list 列出中繼的每一個預覽,每行一個:token 的前 8 個字元、標籤、連接埠或資料夾、離線時加上 — offline,以及連結。沒有預覽時顯示 No live previews.,預覽通道關閉時顯示 The preview lane is disabled on this relay.。
/preview close 關閉這個對話的預覽。沒有預覽時顯示 No live previews in this conversation.。有多個時,會列出符合的預覽並要求你輸入更長的前綴。
/preview close 3f9c 關閉 token 以這 4 到 32 個十六進位字元開頭的預覽,不限對話。顯示 Preview closed: <label>。沒有符合時:No preview matches "3f9c".
/preview 顯示用法說明行;任何它無法解讀的輸入也一樣。

由數字組成的第一個字一律視為連接埠,超出 1 到 65535 的連接埠會顯示用法說明行。若資料夾名為 8080,請輸入 ./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。它需要以中繼的存取 token(config.json 中的 authToken,也就是你登入時用的那個 token)作為 bearer token。請先註冊,以便知道基礎路徑,再在該路徑下啟動 dev server:

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 /api/previews port 或 dir(擇一),可選填 conversationId、host、label。
GET /api/previews 所有預覽。?conversationId=,或 session worker 送出的對話標頭,會把範圍縮小到單一對話。
DELETE /api/previews/<token> 關閉一個預覽。dev server 不受影響。0.9.14 新增 指定了對話的請求(方式同 GET)只能關閉該對話的預覽;關閉其他對話的預覽會被拒絕(403)。

讓 App 在預覽的路徑下運作 #

每個預覽都位於共用預覽主機名稱上的 /test_<token>/ 之下。由 proxy 的行為可以歸納出這條規則:

  • 瀏覽器請求 /test_<token>/…。
  • App 收到的路徑不含這個前綴。
  • 所以 App 在頁面中放的連結和資源網址,必須帶有前綴或是相對網址,而且它仍必須回應不帶前綴的請求。

proxy 在過程中會做這些事:

proxy 會
移除前綴 App 收到 /test_<token> 之後的路徑,連同查詢字串。造訪不帶結尾斜線的 /test_<token> 會被重新導向(301)到加上結尾斜線的同一位址,讓相對網址在預覽內解析。
送出 X-Forwarded-Prefix 不含結尾斜線的前綴。
送出 X-Forwarded-Host 預覽監聽器收到的 Host。
送出 X-Forwarded-Proto 前端 proxy 送來的值;若它沒有送,則為 https。
送出 X-Forwarded-For 連線到監聽器那一方的位址。在同一台機器上的前端 proxy 後面,那就是 proxy。
設定 Host 設為 App 自己的位址(127.0.0.1:5173),供會檢查它的 dev server 使用。
改寫 Location 重新導向到 /x 會變成 /test_<token>/x,重新導向到 App 自己位址的絕對網址也一樣。已帶有前綴的重新導向、以 // 開頭的、相對的,或指向其他網站的重新導向則保持不變。
限定 Set-Cookie 範圍 cookie 會加上 Path=/test_<token>/ 並移除 Domain,因此一個預覽的 cookie 不會傳到另一個預覽。名為 copilot_auth 的 cookie 會被丟棄。
標記回應 加上 X-Robots-Tag: noindex, nofollow, noarchive,並移除 Service-Worker-Allowed。

它不會改寫頁面、樣式表或腳本的內容。在 HTML 中寫入 /assets/app.js 的 App 會向預覽主機名稱的根目錄請求,而那裡沒有任何預覽,瀏覽器會得到 No such preview 頁面(404)。這就表示 App 不知道自己的前綴。

可行的做法 #

  • 相對網址。 頁面使用 ./assets/app.js 這類位址的 App 可以直接運作:頁面位於 /test_<token>/,所以這些位址會在預覽內解析。這是從 proxy 的運作方式推得的,並未逐一在各框架上測試。
  • 讀取前綴。 以不帶前綴的路徑做路由、並從 X-Forwarded-Prefix 建構連結的 App,完全符合這條規則。
  • 建置好的網站。 以相對的基礎路徑或等於前綴的基礎路徑建置,再把輸出資料夾發布為靜態預覽。對 /test_<token>/assets/… 的請求會由該資料夾提供,所以兩種網址都能運作。

無法直接使用的做法 #

讓 App 只回應帶有前綴之請求的工具設定並不適用,因為 proxy 會把前綴移除。

  • 把 base 設為前綴(--base)的 Vite dev server。 已對照中繼所在機器上的 Vite 8.3.1 原始碼確認:dev server 只處理以 base 開頭的路徑。它對 / 的請求回應重新導向到 base,而 proxy 不會改動這個重新導向,所以瀏覽器會陷入迴圈。其他不帶前綴的請求會得到 404,說明伺服器有公開的基礎網址。熱重載只在 base 下的路徑監聽,而被移除前綴的請求不符合這個路徑。請改用建置網站的做法,或改用從根目錄提供內容並使用相對網址的 Vite 設定。
  • 把 router 掛載在前綴上的 Express(app.use(prefix, router))。App 收到的是 /x,而不是 /test_<token>/x,所以掛載點不會符合。請把 router 掛載在 /,並從 X-Forwarded-Prefix 建構連結。

Next.js(basePath、assetPrefix)和 SvelteKit(kit.paths.base)在 OAR 給 agent 的說明中有提到,但這些說明並未對照 proxy 的實際行為檢查過,而且這兩個工具在這裡都無法測試。如果它們的基礎路徑設定讓 App 預期每個請求都帶有前綴,那麼它基於與 Vite 相同的原因並不適用。未經驗證:依賴它之前請先試試看。

App 需要前綴時,請先發布

agent 會在啟動 dev server 之前呼叫 create,這樣如果 App 需要前綴,就能把前綴提供給它。對於使用相對網址就能運作的 App,順序並不重要。

靜態預覽 #

用 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(關閉)按鈕。這個面板在背景任務中有說明。
  • 對話紀錄中的連結卡片。 當預覽在對話的回合執行期間發布時,回覆會附上一張連結卡片,不論是 agent 還是你用 /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 表示預覽已經消失,連結不再能解析。

關閉預覽只會結束連結。dev server 會以原本的背景任務身分繼續執行。

預覽不會自行過期。它會在你關閉它時結束,或 0.9.11 新增 在你刪除它的對話時結束(包括透過重建中繼的快取)。封存對話會讓它的預覽保持運作,並仍列在 Settings → Previews 中。

在同一個對話中再次發布同一個連接埠或資料夾,會傳回已經在運作的那個預覽,連結相同、標籤不變,而不會建立新的預覽。

整個中繼同時最多只能有 8 個運作中的預覽(previews.maxLive)。拒絕訊息會列出最舊的三個,附上各自的發布時間和上次開啟時間,以及關閉它們的方法。

中繼重新啟動之後 0.9.11 新增 #

預覽在中繼重新啟動、更新或當機後都會保留:中繼把它們存在資料庫中,並在啟動時還原,所以你發出去的連結仍然可用。每個預覽都會先重新檢查,若檢查發現它已不再有效就會結束:

  • 資料夾預覽,其資料夾已不存在,或已不在其對話的工作資料夾內;
  • 機器重新開機後的連接埠預覽:dev server 已經不在,而該連接埠之後可能屬於別的程式;
  • 對話已不存在的預覽。

Linux 和 macOS 能可靠地回報重新開機。在 Windows 上,Restart(重新啟動)會算作一次重新開機,但在 fast startup(快速啟動)開啟時(Windows 預設值),Shut down(關機)後再開機則不算:之前的連接埠預覽會回來,並連到當時在該連接埠上監聽的任何程式。不再需要的預覽請關閉。

無法得出結果的檢查——因為資料庫忙碌,或資料夾因為已不存在以外的原因無法讀取——不會結束預覽。不過這段期間它不會被提供:Settings → Previews 和工具的 list 會把它顯示為未提供(unverified),它會計入上限,也可以被關閉。中繼每 15 秒會重新檢查一次,檢查一有結果,就會在原本的連結下重新提供它;在它的對話中再次發布同一個連接埠或資料夾,會立即做到這一點。10 分鐘都沒有結果,預覽就會結束,記錄中也會註明。

連接埠預覽在啟動後也會立即探測一次,讓它的 online 或 offline 徽章保持最新。中繼的記錄以簡短雜湊值稱呼預覽,絕不使用它的 token。另見重新啟動會保留什麼、會失去什麼。

WebSocket #

連接埠預覽會在同一個前綴下轉送 WebSocket 連線,並像任何請求一樣移除前綴,所以當瀏覽器透過預覽連結連線時,App 自己的 socket 可以運作。(熱重載是 App 自己的事:見上面關於 Vite 的說明。)靜態預覽,以及未知或已關閉的連結,會以 404 回應 WebSocket 請求。如果 App 以一般回應來回應升級請求,瀏覽器會得到那個狀態碼。如果 App 停止運作,則為 502。

注意事項 #

  • 預覽之間共用同一個來源。 cookie 限定在各預覽的路徑內,token 也無法猜測,但兩個預覽彼此之間的隔離,不如各自與中繼之間的隔離那麼徹底。
  • 預設只限你自己的機器。 連接埠預覽只能指向迴路位址,再加上 previews.allowedTargetHosts 中的主機。中繼自己的連接埠一律會被拒絕,因為發布其中一個會把中繼的登入頁面放到公開主機名稱上。
  • 中繼不會執行 App。 它從不啟動或重新啟動 dev server。如果伺服器停止,卡片會變成 offline。

依供應商 #

預覽在每個供應商上看起來都一樣;只有 agent 發布的方式不同,如參考頁面所示。/preview 和 Settings → Previews 適用於所有供應商。

疑難排解 #

你看到的情況 原因與解決方法
agent 或 API 回應 Preview lane is disabled 或 Preview lane failed its startup checks(503) 預覽通道已關閉或拒絕啟動。閱讀中繼記錄中的 PREVIEW LANE disabled 行,修正 previews,然後重新啟動中繼。見預覽通道拒絕啟動時。
連結顯示 Preview upstream unreachable 頁面(502) 沒有任何東西在該連接埠上監聽,或連線失敗。在同一個連接埠上重新啟動 dev server 並重新載入;連結仍然有效。
資源缺失或頁面空白,且對 /assets/... 的請求得到 No such preview(404) App 寫入了以根目錄起始的絕對路徑。讓它輸出相對網址或帶前綴的網址(規則)。
頁面不斷循環載入,或 App 回應 404 並提到「public base URL」 App 預期每個請求都帶有前綴,但 proxy 會把它移除(無法直接使用的做法)。
整個連結都顯示 No such preview 連結不明或已關閉、它的對話已被刪除,或重新啟動時發現它已不再有效(重新啟動之後)。請重新發布。
預覽顯示為未提供(unverified) 中繼在啟動時無法檢查它。它每 15 秒會重新檢查一次;在它的對話中再次發布同一個目標,會立即讓它恢復。
連結完全打不開 前端 proxy 或 DNS 尚未設定,或監聽器無法綁定它的連接埠(記錄中的 PREVIEW LANE failed to bind)。
卡片顯示 offline dev server 已停止回應。在同一個連接埠上重新啟動它。
Too many live previews (max 8); previews stay until closed. Oldest: …(429) 在 Settings → Previews 中、用 /preview close,或用 DELETE /api/previews/<token> 關閉一個;agent 可以用工具的 close 動作關閉自己對話的預覽;或調高 previews.maxLive。
Port <n> belongs to the relay and cannot be previewed(400) 你給的是中繼的連接埠、它的 CLI 連接埠或預覽連接埠。請發布 dev server 的連接埠。
Target port must be between 1024 and 65535(400) 使用更高的連接埠。
Target host ... is not loopback and not in previews.allowedTargetHosts(400) 把該主機加到 previews.allowedTargetHosts。
dir must live inside the workspace root (...)、Directory not found: ... 或 Not a directory: ...(400) 資料夾必須存在,且位於對話的工作資料夾內。