1. 什麼是 ChatGPT App 的 smoke 測試
在一般的網頁開發世界,smoke 測試就是最低限度的檢查:「系統還活著嗎?」頁面能打開、按鈕不會崩潰、沒有致命火警。
在 ChatGPT Apps 的世界裡,smoke 測試更有趣一些,因為整條鏈路同時牽涉多個環節:
- 你的 widget 程式碼(React/Next.js)。
- Next.js 的 dev 伺服器。
- 隧道(ngrok/Cloudflare)。
- ChatGPT 會建立 iframe 並把你的 widget 載入聊天室內。
對我們來說,好的 smoke 測試意味著:
- widget 能在 ChatGPT 內無錯誤地渲染;
- 基本互動可用(例如,點擊按鈕——能開啟外部連結);
- 瀏覽器主控台和 dev 伺服器日誌都沒有一片紅色錯誤雪崩。
重要:此階段我們還不檢查 MCP 工具,不做壓力測試,也不計算 token 花費。我們的目標很樸素且實用:證明「程式碼 → Next.js → 隧道 → ChatGPT → 使用者」這條鏈路確實閉合起來。
可以把它想像成一張小表:
| 檢查項目 | 如何判斷沒問題 |
|---|---|
| Widget 渲染 | 在 ChatGPT 能看到我們的 UI,而不是「損壞的 iframe」 |
| ChatGPT ↔ 我們的伺服器 | 沒有「無法載入應用程式」之類的錯誤 |
| 沙盒中的 JS 運作 | 處理器 onClick 會確實執行 |
| 能否開啟外部連結 | 按鈕會以指定 URL 開啟新的分頁/視窗 |
2. 我們的教學 App:簡單的「Hello GiftGenius」
在這門課中我們會逐步打造 GiftGenius——禮物挑選助手。此時它還不會幫你選禮物,但至少能禮貌地打招呼,並顯示「瞭解更多」的連結。
我們需要一個最小但「真實」的 widget:不用複雜邏輯,但包含活生生的 React 程式碼。
最簡單的 widget 元件可以長這樣(名稱和樣式你可以自行調整,但我們先用課程計畫中的基礎版本):
// app/widget/page.tsx
'use client';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
這是你的第一個 ChatGPT App。接下來我們會教它如何挑選禮物。
</p>
</main>
);
}
幾個重要重點。
首先,檔案開頭的指示 'use client'; 會把元件設定為用戶端元件。少了它,Next.js 會把檔案視為伺服器元件,你將無法使用 window、onClick 處理器,以及任何瀏覽器 API。
其次,這就是一般的 React 元件。看不見任何「Apps SDK 的魔法」——而這很誠實。之所以能在 ChatGPT 內出現的「魔法」,其實藏在 MCP 伺服器與工具的設定裡,該工具會回傳 widget URL。這部分我們之後再做,現在只關心 UI。
3. 把 widget 嵌入樣板並啟動
在官方的 Apps SDK Next.js 樣板中,widget 頁面通常已經存在;你可以直接編輯它,或依需求在對應路由下建立自己的頁面(例如 /widget)。
假設你已經有 app/widget/page.tsx,並把它的內容替換為上面的程式碼。接下來的流程是:
- 你儲存檔案。
- Next.js 的 dev 伺服器(已透過 npm run dev 啟動)會重載需要的模組,HMR 會更新頁面。
- 透過隧道,你的公開 HTTPS URL 在同一個路徑 /widget 會開始提供更新後的 UI。
可以用兩種方式驗證。
先用傳統方式——在本機瀏覽器中。打開:
http://localhost:3000/widget
你會看到同樣的 Hello from GiftGenius。這還不是 ChatGPT,只是先確認你的 Next.js 應用的 UI 是活的。
接著透過隧道。拿到發給你的 URL(像是 https://witty-cat.ngrok-free.app),加上 /widget,用一般瀏覽器打開:
https://witty-cat.ngrok-free.app/widget
如果一切正常,頁面看起來應該一樣。代表「Next.js → 隧道 → 你的瀏覽器」這段鏈路沒問題,接著只差把 ChatGPT 插進去了。
4. 在 ChatGPT 內檢查 widget
在 Dev Mode 裡,ChatGPT 基本上做三件事:建立 iframe、把它的 src 指向你的公開 URL,並讓這個 iframe 在聊天訊息裡運作。
簡化後的事件流程如下:
sequenceDiagram
participant Dev as 你(Dev)
participant Next as Next.js dev 伺服器
participant Tun as 隧道(HTTPS)
participant GPT as ChatGPT
participant User as 使用者
Dev->>Next: npm run dev (http://localhost:3000)
Dev->>Tun: 啟動連到 3000 埠的隧道
GPT->>Tun: GET https://.../widget
Tun->>Next: 代理到 http://localhost:3000/widget
Next-->>Tun: Widget 的 HTML + JS
Tun-->>GPT: 回應(HTML/JS)
GPT->>User: 渲染含 widget 的 iframe
要看到結果,你需要:
- 在瀏覽器中打開 ChatGPT,選擇需要的模型(通常是 GPT‑5.1 或 Dev Mode 預設)。
- 明確選擇你的應用(透過 Apps/Developer 菜單),或用一句話「召喚」它,例如:「啟動 GiftGenius 應用」。
- ChatGPT 呼叫你的 App,MCP 伺服器回傳包含 UI 連結(也就是 /widget)的回應,於是你的 widget 就出現在聊天訊息中。
如果一切順利,你會直接在 ChatGPT 內看到熟悉的「Hello from GiftGenius」標題。到這一步,smoke 測試幾乎通過了:iframe 能渲染,「Next.js → 隧道 → ChatGPT」這段鏈路是活的。剩下最後一項——確認 widget 能以可預期方式開啟外部連結。這就需要 openExternal。
稍後當你開始改動程式碼時,正常的開發循環大致會是:
- 修改 JSX。
- 儲存。
- 要嘛重新整理 ChatGPT 分頁,要嘛(有時)只要「喚醒」一下 widget——例如再送一則訊息,或再次啟動 App(依你的樣板與快取設定而定)。
如果看不到變更,先想到三個嫌疑犯:dev 伺服器沒跑、隧道斷了,或 ChatGPT 連到舊的 URL。在「如果有問題,該去哪裡查錯」章節我們會更詳細說明。
5. 為什麼不能直接放 <a href> 就了事
為了完成 smoke 測試最後一項——按鈕能開啟外部頁面——我們得弄懂 openExternal。合理的疑問是:「為什麼需要 openExternal?不能直接做一個普通連結嗎?」
問題在於你的 widget 並不是「單純在瀏覽器裡」,而是由 ChatGPT 控管的 iframe。這個 iframe 在挺嚴格的沙盒下運作:可能會受 Content Security Policy 限制、有 sandbox 屬性、搭配 target="_blank" 的奇妙行為,以及彈出視窗阻擋等等。結果就是在這種 iframe 裡的 <ahref="…"> 或 window.open() 可能會有不可預期的行為:從完全被忽略,到出現你的程式碼無法控制的警告。
此外,就 UX 而言,OpenAI 希望能控制你何時、如何開啟外部頁面。因此 Apps SDK 提供了統一的橋接 window.openai:你的程式碼不會直接去操作父視窗,而是依照明確的 API 把動作委派給宿主應用。
6. API window.openai.openExternal:它是什麼,如何運作
在 widget 的沙盒中可以使用全域物件 window.openai。它是你的 UI 與 ChatGPT 之間的主要「橋梁」:可以透過它呼叫工具、傳送後續訊息、切換顯示模式、管理 widget 狀態,當然也能開啟外部連結。
這堂課我們只關心其中一個方法:
window.openai.openExternal({ href: string }): void;
當你呼叫 window.openai.openExternal({ href: 'https://example.com' }) 時,ChatGPT 會:
- 檢查該 URL 是否符合政策。
- 可能會向使用者顯示警告(例如這是外部網站)。
- 在使用者的瀏覽器中開啟新的分頁/視窗。
有兩點需要注意。
第一,這是純前端操作。它不會呼叫 MCP 工具、不會打你的後端,也不會消耗 OpenAI token。它只是對宿主應用發出一個訊號:「請開啟這個 URL」。
第二,這種方式與沙盒相容。ChatGPT 自行決定要如何開啟連結,而不讓你的 iframe 濫用 window.open()。
7. 在 widget 中加入帶 openExternal 的按鈕
現在我們讓「Hello GiftGenius」可以開啟外部連結。最簡單的情境是:一個「開啟示範連結」的按鈕,導向你的服務文件或著陸頁。
先寫個小 helper,避免 TypeScript 抱怨,並保證你若直接在瀏覽器開 /widget(此時還沒有 window.openai)時,widget 不會崩潰:
// app/widget/openExternalSafe.ts
export function openExternalSafe(href: string) {
if (typeof window !== 'undefined' && (window as any).openai?.openExternal) {
(window as any).openai.openExternal({ href });
} else {
// 在沒有 ChatGPT 的本機預覽時的退路
window.open(href, '_blank', 'noopener,noreferrer');
}
}
這裡我刻意使用 (window as any),以免一開始就讓你煩惱 window.openai 的型別。課程稍後我們會把這個物件的介面好好定義。此刻只需要程式能編譯並運作即可。
現在把 helper 接到我們的 widget,並加上一個按鈕:
// app/widget/page.tsx
'use client';
import { openExternalSafe } from './openExternalSafe';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
這是你的第一個 ChatGPT App。接下來我們會教它如何挑選禮物。
</p>
<button
type="button"
onClick={() => openExternalSafe('https://example.com')}
style={{
padding: '8px 16px',
borderRadius: 8,
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
開啟示範連結
</button>
</main>
);
}
點擊後會發生什麼。
如果 widget 在 ChatGPT 中執行,window.openai.openExternal 會存在,ChatGPT 會依規則開啟 https://example.com。
如果你是在一般瀏覽器打開 http://localhost:3000/widget,這時沒有 window.openai,就會走 fallback:用瀏覽器的方式開新分頁。這裡的 window.open 只在你直接於一般瀏覽器開 /widget(非 ChatGPT 沙盒內)時使用,在這個情境下它能如常運作,不會造成問題。
我們會在模組 3(關於 widget 與沙盒的獨立課)更詳細探討 openExternal,現在可以放心繼續啟動應用。
8. 迷你 end‑to‑end smoke 測試
現在可以進行一次完整的「實戰」跑法。嘗試完成以下步驟:
- 確認 dev 伺服器已啟動(npm run dev),並且你能在 http://localhost:3000/widget 看到 Hello from GiftGenius。
- 確認連到 3000 埠的隧道已建立,且公開 URL 能由外部瀏覽器開啟。
- 打開 ChatGPT,切到 Dev Mode,確認你的 App 指向正確的 URL(公開的,而不是 localhost)。
- 打開聊天,選擇 App(或請模型啟動它)。
- 確認在內嵌的 widget 中能看到「Hello from GiftGenius」。
- 點擊「開啟示範連結」按鈕,確認瀏覽器開啟了 https://example.com(或你的網址)。
若上述皆成功,表示:
- Widget 的 HTML/JS 能被 Next 伺服器正確編譯與提供。
- HTTPS 隧道能正確代理請求。
- ChatGPT 信任你的 URL,並能載入 widget。
- window.openai 正常工作並傳遞開啟外部連結的命令。
這正是我們對第一個 smoke 測試的期待。
9. 出問題時應該去哪裡查錯
與「一般」前端不同,這裡主要有三個診斷位置。關鍵是要快速判斷問題到底出在哪一段:
- 先看 ChatGPT 中的 UI。如果你看到錯誤訊息如「Error loading app」或「We had trouble talking to your app」,通常是隧道或你的 dev 伺服器可用性出了問題。試著直接在瀏覽器打公開 URL:如果打不開,或出現 Next.js 錯誤,先處理這件事。
- 接著開啟運行 ChatGPT 的瀏覽器分頁裡的 DevTools。裡面有一個屬於你 widget 的獨立 iframe,在其中也有熟悉的 Console。如果點擊 openExternal 的按鈕沒有任何反應,看看是否有「window.openai is undefined」或其他 JS 錯誤。若有,通常代表你不是在 ChatGPT 內測(而是直接打隧道 URL)或你忘了加上 'use client';。
- 同時檢查執行 npm run dev 的終端機。如果有編譯錯(TypeScript、ESLint、編譯),在最好情況下 ChatGPT 只能看到舊版程式碼,更糟是什麼都看不到。如果沒有錯誤但你看不到更新,確認隧道仍然存活:很多隧道服務在閒置超時後會關閉工作階段。
還有一種常見情況:在 localhost 一切正常,但透過隧道存取時得到 404 或奇怪的頁面。請仔細檢查基礎路徑(/widget vs /)、basePath/assetPrefix 設定(若你已經動過),以及 Dev Mode 中設定的地址。
10. 關於「收尾」的一點提醒:停止程序
這是小事,但實務上很有用。新手常常忘記 dev 伺服器與隧道是獨立程序,會在背景持續運作。
如果你突然遇到「連接埠 3000 已被占用」,可能是某個終端機裡還藏著舊的 npm run dev。在 Windows 有時就得在工作管理員裡「一番折騰」,在 macOS 與 Linux 上,通常是在啟動該程序的終端機中按 Ctrl + C。
隧道也是如此:如果你連開了好幾個隧道或忘了關掉舊的,很容易搞混 Dev Mode 現在綁的是哪個 URL。養成好習慣:要結束工作階段時——先關隧道、再停 dev 伺服器,下次啟動就從乾淨狀態開始。
11. 首次 smoke 測試常見錯誤
錯誤 1:使用 localhost 而非公開的 HTTPS URL。
很常見的情況是:在 Dev Mode 不小心填了 http://localhost:3000,或根本忘了開隧道。在你的機器上當然沒問題,但住在雲端的 ChatGPT 根本無法連到 localhost。解法很簡單:確認 App 設定的是公開的 HTTPS 隧道地址,且路徑正確(/mcp 或根路徑——視樣板而定)。
錯誤 2:忘了在 widget 檔案加上 'use client';。
你寫了漂亮的 React 程式,加入 onClick,使用 window.openai,但 Next.js 默默把頁面當作伺服器元件。最好的情況你會得到「window is not defined」,更糟是元件根本編不出來。若要使用瀏覽器 API,widget 必須是用戶端元件,而這正是第一行 'use client'; 要表達的。
錯誤 3:直接呼叫 window.open() 而不是 openExternal。
有時看起來做 window.open('https://example.com') 比較簡單。在一般瀏覽器也許還行,但在 ChatGPT 沙盒裡可能出現不可預期的行為:從完全無視到被封鎖。對 ChatGPT Apps 來說,正確做法是 window.openai.openExternal({ href }),把開啟連結的動作委派給宿主並遵守所有安全政策。
錯誤 4:TypeScript 對 window.openai 報錯,開發者用「關掉型別」來處理。
有人會在檔案頂部寫上 // @ts-nocheck。這能消除編譯錯誤,但同時把整個檔案的 TypeScript 都關掉。更安全的做法是對 window 進行局部的 as any,或是在獨立檔案描述最小的 window.openai 介面。我們在本模組選擇了小小的 helper openExternalSafe 搭配 (window as any),之後再補上更嚴謹的型別。
錯誤 5:只在 localhost 檢視結果,沒有在 ChatGPT 內測。
有時會貪圖方便,看到 http://localhost:3000/widget 能開就以為大功告成。但本模組的意義就是要在 ChatGPT 內看到 App。在一般瀏覽器沒問題,並不代表 ChatGPT 能正確建立 iframe、透過隧道載入資源,或不會遇到 CORS/CSP。完整的 smoke 測試一定要包含在 ChatGPT 介面中實際啟動 App 的步驟。
錯誤 6:忘了關、或已掉線的隧道。
你更新了程式碼,但 ChatGPT 仍顯示舊版 widget,或什麼都載不到。常見原因是隧道因為閒置逾時而關了,但 Developer Mode 仍指向舊的 URL。若在一般瀏覽器打開隧道 URL 就看到錯誤——請先把隧道恢復,之後再檢查 Apps SDK。
錯誤 7:忽略 iframe 內的主控台。
做 SPA 的開發者習慣在自己應用的 DevTools 看 console.log,但在 ChatGPT 裡它是 iframe,需要在 DevTools 選到正確的 frame。如果只看最上層,你可能完全看不到錯誤,而 widget 內早已一片紅。養成「在 iframe‑widget 上開 DevTools」的習慣,能大幅省下心力。
GO TO FULL VERSION