CodeGym /課程 /ChatGPT Apps /第一個 smoke 測試:「Hello widget」與 openExternal

第一個 smoke 測試:「Hello widget」與 openExternal

ChatGPT Apps
等級 2 , 課堂 4
開放

1. 什麼是 ChatGPT App 的 smoke 測試

在一般的網頁開發世界,smoke 測試就是最低限度的檢查:「系統還活著嗎?」頁面能打開、按鈕不會崩潰、沒有致命火警。

在 ChatGPT Apps 的世界裡,smoke 測試更有趣一些,因為整條鏈路同時牽涉多個環節:

  1. 你的 widget 程式碼(React/Next.js)。
  2. Next.js 的 dev 伺服器。
  3. 隧道(ngrok/Cloudflare)。
  4. 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 會把檔案視為伺服器元件,你將無法使用 windowonClick 處理器,以及任何瀏覽器 API。

其次,這就是一般的 React 元件。看不見任何「Apps SDK 的魔法」——而這很誠實。之所以能在 ChatGPT 內出現的「魔法」,其實藏在 MCP 伺服器與工具的設定裡,該工具會回傳 widget URL。這部分我們之後再做,現在只關心 UI。

3. 把 widget 嵌入樣板並啟動

在官方的 Apps SDK Next.js 樣板中,widget 頁面通常已經存在;你可以直接編輯它,或依需求在對應路由下建立自己的頁面(例如 /widget)。

假設你已經有 app/widget/page.tsx,並把它的內容替換為上面的程式碼。接下來的流程是:

  1. 你儲存檔案。
  2. Next.js 的 dev 伺服器(已透過 npm run dev 啟動)會重載需要的模組,HMR 會更新頁面。
  3. 透過隧道,你的公開 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

要看到結果,你需要:

  1. 在瀏覽器中打開 ChatGPT,選擇需要的模型(通常是 GPT‑5.1 或 Dev Mode 預設)。
  2. 明確選擇你的應用(透過 Apps/Developer 菜單),或用一句話「召喚」它,例如:「啟動 GiftGenius 應用」。
  3. ChatGPT 呼叫你的 App,MCP 伺服器回傳包含 UI 連結(也就是 /widget)的回應,於是你的 widget 就出現在聊天訊息中。

如果一切順利,你會直接在 ChatGPT 內看到熟悉的「Hello from GiftGenius」標題。到這一步,smoke 測試幾乎通過了:iframe 能渲染,「Next.js → 隧道 → ChatGPT」這段鏈路是活的。剩下最後一項——確認 widget 能以可預期方式開啟外部連結。這就需要 openExternal

稍後當你開始改動程式碼時,正常的開發循環大致會是:

  1. 修改 JSX。
  2. 儲存。
  3. 要嘛重新整理 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 會:

  1. 檢查該 URL 是否符合政策。
  2. 可能會向使用者顯示警告(例如這是外部網站)。
  3. 在使用者的瀏覽器中開啟新的分頁/視窗。

有兩點需要注意。

第一,這是純前端操作。它不會呼叫 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 測試

現在可以進行一次完整的「實戰」跑法。嘗試完成以下步驟:

  1. 確認 dev 伺服器已啟動(npm run dev),並且你能在 http://localhost:3000/widget 看到 Hello from GiftGenius
  2. 確認連到 3000 埠的隧道已建立,且公開 URL 能由外部瀏覽器開啟。
  3. 打開 ChatGPT,切到 Dev Mode,確認你的 App 指向正確的 URL(公開的,而不是 localhost)。
  4. 打開聊天,選擇 App(或請模型啟動它)。
  5. 確認在內嵌的 widget 中能看到「Hello from GiftGenius」。
  6. 點擊「開啟示範連結」按鈕,確認瀏覽器開啟了 https://example.com(或你的網址)。

若上述皆成功,表示:

  • Widget 的 HTML/JS 能被 Next 伺服器正確編譯與提供。
  • HTTPS 隧道能正確代理請求。
  • ChatGPT 信任你的 URL,並能載入 widget。
  • window.openai 正常工作並傳遞開啟外部連結的命令。

這正是我們對第一個 smoke 測試的期待。

9. 出問題時應該去哪裡查錯

與「一般」前端不同,這裡主要有三個診斷位置。關鍵是要快速判斷問題到底出在哪一段:

  1. 先看 ChatGPT 中的 UI。如果你看到錯誤訊息如「Error loading app」或「We had trouble talking to your app」,通常是隧道或你的 dev 伺服器可用性出了問題。試著直接在瀏覽器打公開 URL:如果打不開,或出現 Next.js 錯誤,先處理這件事。
  2. 接著開啟運行 ChatGPT 的瀏覽器分頁裡的 DevTools。裡面有一個屬於你 widget 的獨立 iframe,在其中也有熟悉的 Console。如果點擊 openExternal 的按鈕沒有任何反應,看看是否有「window.openai is undefined」或其他 JS 錯誤。若有,通常代表你不是在 ChatGPT 內測(而是直接打隧道 URL)或你忘了加上 'use client';
  3. 同時檢查執行 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」的習慣,能大幅省下心力。

1
問卷/小測驗
第一個 ChatGPT 應用程式,等級 2,課堂 4
未開放
第一個 ChatGPT 應用程式
第一個 ChatGPT 應用程式:範本、Dev Mode、隧道
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION