1. Sandbox là gì và vì sao widget của bạn bị “nhốt”
Khi ChatGPT hiển thị widget của bạn, nó render không như một <iframe src="https://your-site"> thông thường. Widget chạy trong một “sandbox” được quản lý — một iframe cách ly với origin riêng và các cài đặt bảo mật nghiêm ngặt.
Về kỹ thuật, nó trông đại khái như sau:
flowchart TD
User["Người dùng trong ChatGPT"]
Chat["ChatGPT UI + mô hình"]
Iframe["Widget của bạn
sandboxed iframe"]
MCP["MCP/backend của bạn"]
User --> Chat
Chat -->|gọi tool| MCP
MCP -->|structuredContent + _meta| Chat
Chat -->|window.openai.*| Iframe
Iframe -->|callTool / follow-up| Chat
Chat --> MCP
Mã của bạn chỉ chạy bên trong iframe này, và truy cập ra “thế giới bên ngoài” đi qua một API được kiểm soát chặt do host (ChatGPT) cung cấp. Widget không được phép:
- làm hỏng chính ChatGPT (DOM, styles, hiệu năng);
- vi phạm quyền riêng tư của người dùng;
- tự do gọi mạng không kiểm soát.
Từ đó dẫn đến các giới hạn cốt lõi của sandbox.
Cô lập DOM và origin
Widget sống trên một domain sandbox chuyên dụng (ví dụ, https://sandbox-apps.oaiusercontent.com), với thuộc tính sandbox trên iframe. Điều này có nghĩa là:
- bạn không thể chạm vào window.parent hay document của ChatGPT — sẽ nhận SecurityError;
- những thứ cross-domain như postMessage được host kiểm soát;
- mọi nỗ lực “sửa UI ChatGPT bằng một stylesheet CSS” đều sẽ thất bại.
Mạng và giới hạn CSP
Trình duyệt và CSP của host giới hạn quyền truy cập mạng cho widget của bạn:
- phương thức fetch chỉ có quyền truy cập duy nhất tới các domain trong whitelist, và các domain này phải vượt qua review;
- bạn khai báo rõ các domain có thể truy cập từ widget thông qua openai/widgetCSP trong phản hồi từ MCP; nếu không, các request sẽ bị chặn;
- hướng khuyến nghị cho mọi tác vụ nghiêm túc là không gọi mạng trực tiếp từ widget, mà đi qua backend bằng MCP tools và callTool (chi tiết ở Mô-đun 4).
Thực tế: hãy coi widget như một lớp UI mỏng. Nó giao tiếp với ChatGPT và server của bạn qua các kênh được xác định rõ ràng, chứ không phải một SPA tự do trên internet.
Lưu trữ cục bộ và tài nguyên
Các kho cục bộ (localStorage, sessionStorage) sẵn dùng, còn cookie thì không. Hãy lưu ý điều này khi phát triển ứng dụng. Bộ nhớ và CPU bị giới hạn: nếu bạn quyết định tính lại tất cả số nguyên tố tới một tỷ ngay trong widget, host hoàn toàn có quyền “giết” iframe của bạn.
Từ đó rút ra điều quan trọng: không làm tính toán nặng và “cache” sống lâu trong widget. Logic phức tạp nên ở phía server, không phải trong React component.
2. window.openai: cây cầu giữa widget và ChatGPT
Để widget có thể biết được điều gì đó (kết quả tool, chế độ hiển thị, locale, trạng thái), khi khởi tạo ChatGPT sẽ nhúng vào cửa sổ của iframe một đối tượng global — window.openai.
Đây không phải mã của bạn hay gói npm, mà là một host object do chính nền tảng AI cung cấp. Bên dưới nó dựa trên events và messaging giữa host và iframe, nhưng bạn hầu như không cần bận tâm. Quan trọng là nhớ vài điểm.
Ai tạo và khi nào có window.openai
window.openai chỉ xuất hiện:
- bên trong đúng iframe mà ChatGPT tạo cho widget của bạn;
- khi HTML template được trả về với mimeType phù hợp (text/html+skybridge) và vượt qua mọi kiểm tra.
Bạn đã thấy loại này trong mô-đun về HelloWorld App — đó là thứ mà trang widget trả về thay cho text/html thông thường.
Nếu bạn mở trang widget trực tiếp trong trình duyệt, thì:
console.log(window.openai); // undefined
và điều đó là bình thường. Vì vậy trong mã của widget luôn nên kiểm tra sự tồn tại của đối tượng này nếu bạn trông đợi “chế độ standalone” cho phát triển local hoặc storybook.
Ví dụ sơ khai (không phải bản cuối, chỉ minh họa):
if (typeof window !== "undefined" && (window as any).openai) {
console.log("We are inside ChatGPT sandbox!");
}
Tính bất đồng bộ của khởi tạo
Bên dưới, ChatGPT cập nhật window.openai khi có dữ liệu mới (một toolOutput mới, đổi displayMode v.v.) bằng cách sử dụng sự kiện nội bộ openai:set_globals.
Tức là “giá trị” trong đó không tĩnh: mô hình AI có thể gọi MCP tool, backend trả về structuredContent mới, và window.openai.toolOutput sẽ thay đổi ngay dưới React component của bạn.
Hai khuyến nghị:
- Đừng tạo các snapshot “mù” kiểu const toolOutput = window.openai.toolOutput một lần lúc đầu và nghĩ rằng nó bất biến. Cùng một widget có thể được ChatGPT tái sử dụng.
- Hãy dùng lớp hook (ở phần sau), lớp này biết cách subscribe vào các thay đổi.
3. Giải phẫu window.openai: dữ liệu, API và ngữ cảnh
Tài liệu chính thức cung cấp một bảng khá gọn về các field và method của window.openai. Ta gom lại theo cách “dễ đọc” hơn.
Các field và method chính
window.openai = {
// State & data
toolInput, // JSON: các tham số mà AI đã truyền vào MCP-tool của bạn
toolOutput, // JSON: các tham số mà MCP-tool của bạn trả về cho AI
toolResponseMetadata, // Phần phản hồi MCP-tool: phần _meta: {...}
widgetState, // Có thể đọc trạng thái đã lưu của widget
setWidgetState, // Có thể lưu trạng thái của widget vào đây
// Runtime APIs
callTool, // Gọi MCP-tool
sendFollowUpMessage, // Âm thầm gửi một tin nhắn vào chat cho AI; nó sẽ bắt đầu trả lời.
requestDisplayMode, // Chuyển widget sang chế độ khác: fullscreen, pip, inline
requestModal, // Mở widget dưới dạng modal.
requestClose, // Đóng widget. Nếu đang là modal, đóng modal và trở lại widget.
requestCheckout, // Mở modal thanh toán. Server phải triển khai ACP
notifyIntrinsicHeight, // Thông báo thay đổi chiều cao nội tại của widget
openExternal, // Mở liên kết trong cửa sổ mới.
// Context
theme, // Chủ đề tối hay sáng
displayMode, // Chế độ hiển thị hiện tại của widget; có thể khác với requestDisplayMode
maxHeight, // Chiều cao tối đa cho phép của widget
safeArea, // "Vùng an toàn" khi vẽ — hữu ích cho điện thoại có "notch"
view,
userAgent, // userAgent của trình duyệt
locale // locale của trình duyệt
}
Bảng tóm tắt tương tự:
| Danh mục | Thuộc tính / phương thức | Để làm gì |
|---|---|---|
| State & data | |
Các tham số khi tool được gọi. Read-only. |
| State & data | |
structuredContent của bạn từ phản hồi MCP. Thứ mà widget và mô hình nhìn thấy. |
| State & data | |
_meta từ phản hồi. Chỉ widget thấy; mô hình không đọc phần này. |
| State & data | |
Ảnh chụp trạng thái UI mà ChatGPT lưu giữa các lần render của widget. |
| State & data | |
Lưu ảnh chụp widgetState mới một cách đồng bộ. |
| Function | |
Gọi MCP tool từ widget. |
| Function | |
Yêu cầu ChatGPT gửi một tin nhắn vào chat thay mặt widget. Nó sẽ bắt đầu trả lời. |
| Function | |
Xin host chuyển sang inline / fullscreen / pip. |
| Function | |
Yêu cầu mở cửa sổ modal. |
| Function | |
Báo rằng chiều cao nội dung đã thay đổi. |
| Function | |
Mở hộp thoại thanh toán theo giao thức ACP. |
| Function | |
Mở liên kết ngoài trên trình duyệt người dùng. |
| Context | |
Tín hiệu môi trường: chủ đề, chế độ, chiều cao khả dụng, locale, v.v. |
Bạn không cần ghi nhớ tất cả ngay — hãy coi bảng trên như “bản đồ khu vực”. Giờ ta sẽ đi theo cách thực tiễn hơn thay vì “sổ tay tra cứu”.
toolInput và toolOutput: dữ liệu đến từ đâu
Khi mô hình quyết định gọi tool của bạn, nó tạo ra các tham số JSON. Các tham số này:
- đến MCP server như input cho handler;
- đồng thời xuất hiện trong window.openai.toolInput trong widget.
Sau khi tool chạy xong, server trả về:
- structuredContent — dữ liệu có cấu trúc cho UI;
- _meta — dữ liệu riêng chỉ cho widget;
- content — văn bản cho chính mô hình để “kể” cho người dùng biết điều gì đã xảy ra.
structuredContent trở thành window.openai.toolOutput, còn _meta trở thành window.openai.toolResponseMetadata.
Ví dụ mini (JS thuần, không dùng React):
const root = document.getElementById("root");
// Có thể dùng toán tử nullish một cách an toàn
const gifts = window.openai.toolOutput?.gifts ?? [];
root.textContent = `Số quà tặng tìm được: ${gifts.length}`;
widgetState và setWidgetState: “bộ nhớ” của widget
widgetState là những gì nền tảng sẵn sàng ghi nhớ về UI của bạn giữa các lần render, thậm chí giữa các bước trong cuộc hội thoại.
Ví dụ hợp lý cho widgetState:
- món quà đã chọn;
- sắp xếp hiện tại (theo giá / theo độ phổ biến);
- trang hiện tại trong danh sách.
Ví dụ không hợp lý:
- kết quả thô từ API bên thứ ba;
- ảnh ở dạng base64;
- token bí mật.
Cần nhớ hai điều:
- widgetState được lưu và truyền cho mô hình cùng với ngữ cảnh, nên đừng đặt thông tin nhạy cảm vào đó.
- Dung lượng có hạn (khoảng 4 nghìn token), vì thế đừng biến nó thành một cơ sở dữ liệu mini.
Ví dụ đơn giản (thẳng tay, không hook, JS thuần):
const current = window.openai.widgetState ?? { selectedGiftId: null };
function selectGift(id) {
window.openai.setWidgetState({ ...current, selectedGiftId: id });
}
Trong mã thực tế, ta sẽ bọc nó bằng các React hook.
Runtime API: callTool, sendFollowUpMessage và các hàm tương tự
Các phương thức này cho phép widget không chỉ “vẽ UI” mà còn tương tác với cuộc hội thoại và server.
Một vài kịch bản phổ biến:
- callTool("search_gifts", { budget: 50 }) — người dùng bấm nút “Thay đổi ngân sách”, bạn gọi server và cập nhật UI;
- sendFollowUpMessage({ prompt: "Hiển thị thêm những ý tưởng đắt hơn" }) — thay vì yêu cầu người dùng gõ thủ công, bạn thêm một nút follow-up để tạo tin nhắn mới trong chat;
- requestDisplayMode({ mode: "fullscreen" }) — nếu chế độ inline trở nên chật chội, widget có thể lịch sự yêu cầu ChatGPT mở toàn màn hình;
- openExternal({ href: "https://myshop.com/checkout?giftId=123" }) — đưa người dùng ra trang ngoài (checkout, hồ sơ, v.v.) qua kênh đã kiểm duyệt.
Tất cả đều đi “qua dây” nhờ ChatGPT, không kết nối thẳng ra internet.
Ngữ cảnh môi trường: theme, chế độ, chiều cao, locale
Các field như theme, displayMode, maxHeight, locale cho bạn biết widget đang chạy trong bối cảnh nào.
Ví dụ:
const theme = window.openai.theme; // "light" hoặc "dark"
const mode = window.openai.displayMode; // "inline" | "fullscreen" | "pip"
const maxH = window.openai.maxHeight; // chiều cao khả dụng
const locale = window.openai.locale; // "en-US", "de-DE", ...
Với các tín hiệu này bạn có thể:
- điều chỉnh màu sắc và khoảng cách theo theme;
- đổi layout tùy chế độ (inline vs fullscreen);
- bản địa hóa nhãn trong UI theo ngôn ngữ người dùng (sẽ có một mô-đun riêng về chủ đề này).
Nền tảng cho tín hiệu về không gian hiển thị, theme và locale. Hợp lý nhất là dùng chúng qua useOpenAIGlobal, useDisplayMode, useMaxHeight và các hook khác để widget trông “hợp ngữ cảnh” trong ChatGPT.
4. Các hook bọc quanh window.openai: đừng chạm trực tiếp đối tượng global
Truy cập trực tiếp window.openai tiện cho prototype, nhưng sẽ nhanh chóng biến mã thành “mì ăn liền”: đăng ký sự kiện, kiểm tra undefined, các wrapper trùng lặp. Vì thế trong template Next.js cho Apps SDK đã có sẵn bộ React hook che giấu chi tiết và khiến mọi thứ trở nên reactive.
Chỉ mục hook điển hình trông như sau:
// app/hooks/openai/index.ts
export { useCallTool } from "./use-call-tool";
export { useSendMessage } from "./use-send-message";
export { useOpenExternal } from "./use-open-external";
export { useRequestDisplayMode, useRequestModal, useRequestClose } from "./use-request-display-mode";
export { useRequestCheckout } from "./use-request-checkout";
// State hooks
export { useDisplayMode } from "./use-display-mode";
export { useWidgetProps } from "./use-widget-props";
export { useWidgetState } from "./use-widget-state";
export { useOpenAIGlobal } from "./use-openai-global";
export { useMaxHeight } from "./use-max-height";
export { useIsChatGptApp } from "./use-is-chatgpt-app";
Tên và đường dẫn có thể hơi khác trong template của bạn, nhưng ý tưởng giống nhau: thay vì window.openai.* hãy dùng hook. Cùng xem các hook chính.
useWidgetProps: đầu vào và đầu ra của tool
useWidgetProps thường trả về một object gồm dữ liệu cần cho widget: toolInput, toolOutput, toolResponseMetadata và đôi khi có các cờ như isLoading.
Ví dụ:
import { useWidgetProps } from "../hooks/openai";
type Gift = { id: string; title: string; price: number };
export function GiftList() {
const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
const gifts = toolOutput?.gifts ?? [];
if (!gifts.length) {
return <div>Chưa có gợi ý quà nào.</div>;
}
return (
<ul>
{gifts.map((g) => (
<li key={g.id}>{g.title} — ${g.price}</li>
))}
</ul>
);
}
Không có window.openai trong mã component — và đó là điều tốt.
useWidgetState: “lớp bọc reactive” trên widgetState
useWidgetState cho phép làm việc với widgetState như React state thông thường: bạn nhận [state, setState], và hook sẽ đồng bộ với window.openai.widgetState và setWidgetState.
Ví dụ:
import { useWidgetState } from "../hooks/openai";
type UiState = { selectedGiftId: string | null };
export function SelectedGiftIndicator() {
const [uiState, setUiState] = useWidgetState<UiState>(() => ({
selectedGiftId: null,
}));
if (!uiState?.selectedGiftId) {
return <div>Chưa chọn quà.</div>;
}
return (
<div>
Bạn đã chọn quà có id={uiState.selectedGiftId}
<button onClick={() => setUiState({ selectedGiftId: null })}>
Đặt lại
</button>
</div>
);
}
Sau khi click, setUiState không chỉ cập nhật React state mà còn lưu trạng thái mới ở phía ChatGPT.
useOpenAIGlobal: truy cập bất kỳ field nào của window.openai
Khi cần lấy một field global (ví dụ theme hay chế độ), có hook tổng quát useOpenAIGlobal(key). Nó subscribe vào sự kiện openai:set_globals và luôn trả về giá trị cập nhật.
Ví dụ:
import { useOpenAIGlobal } from "../hooks/openai";
export function ThemeAwareBlock() {
const theme = useOpenAIGlobal<"light" | "dark">("theme");
const background = theme === "dark" ? "#222" : "#fff";
const color = theme === "dark" ? "#fff" : "#000";
return <div style={{ background, color }}>Tôi tôn trọng theme của ChatGPT</div>;
}
useCallTool, useSendMessage, useOpenExternal và các hook khác
- useCallTool(name) — trả về hàm gọi MCP tool với tên chỉ định. Đây là lớp bọc quanh callTool.
- useSendMessage() — bọc sendFollowUpMessage để widget có thể khởi tạo tin nhắn.
- useOpenExternal() — trợ giúp tiện lợi quanh openExternal({ href }).
- useRequestDisplayMode() và useRequestModal() — bọc việc yêu cầu đổi chế độ / mở modal.
Ví dụ cơ bản cho mini-widget GiftGenius, sử dụng hầu hết mọi thứ ngay lập tức:
import {
useWidgetProps,
useWidgetState,
useCallTool,
useSendMessage,
useOpenExternal,
} from "../hooks/openai";
type Gift = { id: string; title: string; url: string; price: number };
export function GiftWidget() {
const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
const gifts = toolOutput?.gifts ?? [];
const [ui, setUi] = useWidgetState<{ selectedId: string | null }>(() => ({
selectedId: null,
}));
const callSearch = useCallTool("search_gifts");
const sendMessage = useSendMessage();
const openExternal = useOpenExternal();
if (!gifts.length) {
return <div>Chưa có ý tưởng nào. Hãy thử yêu cầu GPT làm mới kết quả.</div>;
}
return (
<div>
{gifts.map((g) => (
<button
key={g.id}
style={{
display: "block",
fontWeight: ui?.selectedId === g.id ? "bold" : "normal",
}}
onClick={() => setUi({ selectedId: g.id })}
>
{g.title} — ${g.price}
</button>
))}
<div style={{ marginTop: 12 }}>
<button
onClick={() =>
sendMessage({ prompt: "Hãy cho tôi các món quà đắt hơn hiện tại." })
}
>
Yêu cầu thêm ý tưởng
</button>
<button
onClick={async () => {
await callSearch({ budget: 200 });
}}
>
Cập nhật với ngân sách $200
</button>
{ui?.selectedId && (
<button
onClick={() =>
openExternal({
href: `https://giftgenius.example.com/checkout?id=${ui.selectedId}`,
})
}
>
Đi tới mua hàng
</button>
)}
</div>
</div>
);
}
Trang này còn thô (các mô-đun sau sẽ hoàn thiện UX, xử lý lỗi v.v.), nhưng đã minh họa được cách tiếp cận: không gọi trực tiếp window.openai, chỉ dùng hook.
5. Thực hành: khám phá sandbox và window.openai
Để “cảm” được việc “widget không phải website bình thường”, hãy làm vài bài tập nhỏ.
Bài tập: “Sờ thử môi trường”
Lấy app/page.tsx hiện tại trong widget và thêm một effect đơn giản khi render lần đầu:
import { useEffect } from "react";
import { useIsChatGptApp } from "../hooks/openai";
export default function Root() {
const isChatGpt = useIsChatGptApp();
useEffect(() => {
if (typeof window !== "undefined") {
console.log("window.origin =", window.origin);
console.log("window.openai =", (window as any).openai);
}
}, []);
return (
<main>
<h1>GiftGenius widget</h1>
<p>Chạy bên trong ChatGPT: {String(isChatGpt)}</p>
</main>
);
}
Mở DevTools: hoặc ngay trong cửa sổ ChatGPT (qua viewer tích hợp của đường hầm nếu hỗ trợ), hoặc trong trình duyệt local khi mở trang trực tiếp. Ở cả hai cách, hãy so sánh:
- khi chạy trong trình duyệt thông thường isChatGptApp sẽ là false, và window.openai rất có thể undefined;
- khi chạy qua ChatGPT bạn sẽ thấy đối tượng với các field toolInput, toolOutput, theme v.v.
Đây là cảm nhận trực quan tốt: cùng một mã React nhưng hành xử khác nhau tùy môi trường, và đó là lý do các hook được tạo ra.
Bài tập: “In ra mọi thứ nền tảng cung cấp”
Thêm một component tạm để debug:
import { useWidgetProps, useOpenAIGlobal } from "../hooks/openai";
export function DebugPanel() {
const { toolInput, toolOutput, toolResponseMetadata } = useWidgetProps();
const theme = useOpenAIGlobal("theme");
const displayMode = useOpenAIGlobal("displayMode");
return (
<pre style={{ fontSize: 10, maxHeight: 200, overflow: "auto" }}>
{JSON.stringify(
{ toolInput, toolOutput, toolResponseMetadata, theme, displayMode },
null,
2
)}
</pre>
);
}
Và tạm thời chèn <DebugPanel /> dưới UI chính. Bạn sẽ thấy trực quan:
- chính xác các field nào từ MCP đi vào toolOutput;
- có gì trong _meta (ví dụ locale, userLocation, v.v.);
- displayMode thay đổi ra sao khi bạn mở rộng widget.
Sau đó có thể bỏ component này, hoặc giữ lại nhưng bật qua một flag kiểu DEBUG_WIDGET.
6. Quan hệ: ChatGPT ↔ widget ↔ MCP/server
Để tránh coi widget là “nhân vật chính” của hệ thống, hãy nhắc lại vai trò các bên.
- Người dùng viết: “Hãy chọn quà cho bạn gái, ngân sách 50$”.
- Mô hình ChatGPT quyết định gọi MCP tool của bạn search_gifts với tham số { recipient: "girlfriend", budget: 50 }.
- MCP server thực thi nghiệp vụ và trả về:
- content với mô tả ngắn cho mô hình;
- structuredContent với mảng quà tặng;
- _meta với chi tiết kỹ thuật (ví dụ nguồn và đơn vị tiền tệ).
- ChatGPT:
- hiển thị cho người dùng tin nhắn văn bản (“Tôi đã tìm thấy vài lựa chọn...”);
- tạo iframe cho widget và truyền structuredContent cùng _meta vào đó thông qua window.openai.toolOutput và toolResponseMetadata.
- Widget của bạn:
- render UI theo toolOutput;
- khi tương tác thì gọi callTool hoặc gửi follow-up.
- Mô hình sẽ quyết định bước tiếp theo với các kết quả đó.
Tất cả điều này dẫn đến một ý quan trọng: widget không bao giờ là “ông chủ” duy nhất của tiến trình. Nó là lớp UI sống trong hệ sinh thái gồm mô hình và MCP server. Những việc phức tạp (ủy quyền, truy cập dữ liệu riêng tư, nghiệp vụ nghiêm túc) nên để ở phía server. Widget chịu trách nhiệm cho giao diện thân thiện và giao tiếp cẩn trọng với người dùng.
7. Chính sách và luật chơi trong sandbox
Toàn bộ cấu trúc với iframe cách ly và window.openai tồn tại vì yêu cầu bảo mật và quyền riêng tư. Các hướng dẫn chính thức của OpenAI nhấn mạnh vài nguyên tắc.
Thứ nhất, tối thiểu hóa dữ liệu. Bạn không nên cố “vòi” càng nhiều PII (thông tin nhận dạng cá nhân) từ người dùng và kéo về phía mình. Những gì thật sự cần thiết phải được mô tả rõ trong tool; cả mô hình lẫn lớp bảo mật sẽ xem xét kỹ các lời gọi như vậy.
Thứ hai, cấm tracking ẩn và fingerprinting. Không được xây hệ thống “nhìn lén” thiết bị người dùng, thu thập dấu vân trình duyệt hay tìm cách vượt hạn chế. Các tham số như userAgent, userLocation, v.v. — là gợi ý cho UX, không phải để ủy quyền hay nhận dạng.
Thứ ba, mọi thứ bạn đẩy vào structuredContent, _meta, widgetState ở một mức nào đó hoặc do người dùng thấy, hoặc có thể được reviewer của Store xem. Vì vậy:
- không được đặt API key, token, mật khẩu hay bí mật admin vào đó;
- trạng thái widget nên được thiết kế sao cho người dùng không ngạc nhiên nếu nhìn thấy trong log hay khi debug.
Thứ tư, các lời gọi mạng. Request trực tiếp từ widget tới API bên ngoài chỉ được phép tới danh sách domain rất hạn chế và trong các kịch bản không nhạy cảm. Hễ dính đến tiền bạc, tài khoản, dữ liệu cá nhân — tất cả phải đi qua MCP/backend.
8. Lỗi thường gặp khi làm việc trong sandbox và với window.openai
Lỗi số 1: nghĩ rằng widget là “website bình thường trong iframe”.
Người mới thường có thói quen chạm vào window.parent, đổi style của ChatGPT hay dùng localStorage như mọi khi. Trong sandbox những việc này hoặc không chạy, hoặc chạy chập chờn: origin khác, storage bị cô lập, truy cập DOM bị chặn. Hãy chấp nhận rằng bạn sống trong môi trường được kiểm soát và chỉ giao tiếp với host qua window.openai và các hook.
Lỗi số 2: đụng trực tiếp window.openai khắp nơi.
Mã kiểu window.openai.toolOutput trong cả chục component sẽ dẫn tới ứng dụng khó debug. Bạn sẽ tự phải theo dõi events, bất đồng bộ và các kiểm tra undefined. Tốt hơn là dùng ngay useWidgetProps, useWidgetState, useOpenAIGlobal và các hook khác vốn đã bọc openai:set_globals và đồng bộ trạng thái.
Lỗi số 3: nhét mọi thứ (đặc biệt là bí mật) vào widgetState.
Đôi lúc “tiện tay” muốn bỏ vào đó một object khổng lồ chứa kết quả API, hoặc thậm chí token truy cập. Hậu quả là ngữ cảnh phình to, mô hình hoạt động kém đi, và bạn vi phạm yêu cầu bảo mật cơ bản. widgetState phải nhỏ gọn, chỉ chứa tín hiệu UI, và tuyệt đối không có dữ liệu nhạy cảm.
Lỗi số 4: cố gắng gọi internet trực tiếp từ widget.
Gọi fetch("https://api.superbank.com/...") từ sandbox gần như chắc chắn vấp CORS, và kể cả bạn cấu hình hoàn hảo, đây vẫn là cách thiếu an toàn và khó kiểm soát. Mọi thứ liên quan tài khoản, tiền bạc, dữ liệu cá nhân cần được hiện thực như MCP tool và gọi qua callTool hoặc phần server.
Lỗi số 5: trông chờ window.openai ổn định ngoài ChatGPT.
Đôi khi lập trình viên thử chạy widget như một SPA độc lập và không thêm kiểm tra rằng window.openai có thể là undefined. Trong môi trường dev, điều này dẫn đến crash “Cannot read properties of undefined”. Hãy dùng useIsChatGptApp, kiểm tra typeof window !== "undefined" và hiển thị UI dự phòng khi không có “widget” thực sự.
Lỗi số 6: phớt lờ ngữ cảnh môi trường (theme, displayMode, maxHeight, locale).
Bạn có thể đặt cứng chiều cao 2000px, luôn bật dark theme và chỉ thiết kế cho desktop — nhưng trải nghiệm người dùng sẽ rất kỳ quặc. Nền tảng cho bạn tín hiệu về khoảng trống, theme và locale — hãy tận dụng chúng qua useOpenAIGlobal, useDisplayMode, useMaxHeight v.v., để widget trông “hợp cảnh” trong ChatGPT.
Lỗi số 7: tìm cách “lách” chính sách bằng script bên ngoài.
Đôi khi có cám dỗ kéo về một tracker, bundle JS bên ngoài hoặc chạy mã từ domain khác “một cách êm lặng”. Sandbox và CSP được làm ra để ngăn điều đó: script bên ngoài sẽ bị chặn, và cố lách hệ thống là con đường thẳng tới việc App của bạn bị từ chối trên Store.
GO TO FULL VERSION